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.
Files changed (137) hide show
  1. {plain_models-0.42.0 → plain_models-0.43.0}/PKG-INFO +1 -1
  2. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/CHANGELOG.md +15 -0
  3. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/base.py +3 -2
  4. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/related.py +6 -28
  5. plain_models-0.43.0/plain/models/fields/related_descriptors.py +393 -0
  6. plain_models-0.43.0/plain/models/fields/related_managers.py +629 -0
  7. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/reverse_related.py +4 -7
  8. {plain_models-0.42.0 → plain_models-0.43.0}/pyproject.toml +1 -1
  9. {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/models.py +3 -1
  10. plain_models-0.42.0/plain/models/fields/related_descriptors.py +0 -947
  11. {plain_models-0.42.0 → plain_models-0.43.0}/.gitignore +0 -0
  12. {plain_models-0.42.0 → plain_models-0.43.0}/LICENSE +0 -0
  13. {plain_models-0.42.0 → plain_models-0.43.0}/README.md +0 -0
  14. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/AGENTS.md +0 -0
  15. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/README.md +0 -0
  16. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/__init__.py +0 -0
  17. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/aggregates.py +0 -0
  18. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/__init__.py +0 -0
  19. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/__init__.py +0 -0
  20. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/base.py +0 -0
  21. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/client.py +0 -0
  22. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/creation.py +0 -0
  23. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/features.py +0 -0
  24. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/introspection.py +0 -0
  25. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/operations.py +0 -0
  26. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/schema.py +0 -0
  27. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/base/validation.py +0 -0
  28. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/ddl_references.py +0 -0
  29. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/__init__.py +0 -0
  30. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/base.py +0 -0
  31. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/client.py +0 -0
  32. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/compiler.py +0 -0
  33. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/creation.py +0 -0
  34. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/features.py +0 -0
  35. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/introspection.py +0 -0
  36. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/operations.py +0 -0
  37. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/schema.py +0 -0
  38. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/mysql/validation.py +0 -0
  39. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/__init__.py +0 -0
  40. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/base.py +0 -0
  41. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/client.py +0 -0
  42. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/creation.py +0 -0
  43. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/features.py +0 -0
  44. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/introspection.py +0 -0
  45. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/operations.py +0 -0
  46. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/postgresql/schema.py +0 -0
  47. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/__init__.py +0 -0
  48. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/_functions.py +0 -0
  49. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/base.py +0 -0
  50. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/client.py +0 -0
  51. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/creation.py +0 -0
  52. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/features.py +0 -0
  53. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/introspection.py +0 -0
  54. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/operations.py +0 -0
  55. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/sqlite3/schema.py +0 -0
  56. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backends/utils.py +0 -0
  57. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backups/__init__.py +0 -0
  58. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backups/cli.py +0 -0
  59. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backups/clients.py +0 -0
  60. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/backups/core.py +0 -0
  61. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/cli.py +0 -0
  62. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/config.py +0 -0
  63. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/connections.py +0 -0
  64. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/constants.py +0 -0
  65. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/constraints.py +0 -0
  66. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/database_url.py +0 -0
  67. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/db.py +0 -0
  68. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/default_settings.py +0 -0
  69. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/deletion.py +0 -0
  70. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/entrypoints.py +0 -0
  71. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/enums.py +0 -0
  72. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/exceptions.py +0 -0
  73. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/expressions.py +0 -0
  74. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/__init__.py +0 -0
  75. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/json.py +0 -0
  76. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/mixins.py +0 -0
  77. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/fields/related_lookups.py +0 -0
  78. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/forms.py +0 -0
  79. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/__init__.py +0 -0
  80. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/comparison.py +0 -0
  81. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/datetime.py +0 -0
  82. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/math.py +0 -0
  83. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/mixins.py +0 -0
  84. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/text.py +0 -0
  85. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/functions/window.py +0 -0
  86. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/indexes.py +0 -0
  87. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/lookups.py +0 -0
  88. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/__init__.py +0 -0
  89. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/autodetector.py +0 -0
  90. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/exceptions.py +0 -0
  91. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/executor.py +0 -0
  92. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/graph.py +0 -0
  93. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/loader.py +0 -0
  94. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/migration.py +0 -0
  95. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/__init__.py +0 -0
  96. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/base.py +0 -0
  97. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/fields.py +0 -0
  98. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/models.py +0 -0
  99. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/operations/special.py +0 -0
  100. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/optimizer.py +0 -0
  101. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/questioner.py +0 -0
  102. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/recorder.py +0 -0
  103. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/serializer.py +0 -0
  104. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/state.py +0 -0
  105. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/utils.py +0 -0
  106. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/migrations/writer.py +0 -0
  107. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/options.py +0 -0
  108. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/otel.py +0 -0
  109. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/preflight.py +0 -0
  110. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/query.py +0 -0
  111. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/query_utils.py +0 -0
  112. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/registry.py +0 -0
  113. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/__init__.py +0 -0
  114. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/compiler.py +0 -0
  115. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/constants.py +0 -0
  116. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/datastructures.py +0 -0
  117. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/query.py +0 -0
  118. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/subqueries.py +0 -0
  119. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/sql/where.py +0 -0
  120. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/test/__init__.py +0 -0
  121. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/test/pytest.py +0 -0
  122. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/test/utils.py +0 -0
  123. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/transaction.py +0 -0
  124. {plain_models-0.42.0 → plain_models-0.43.0}/plain/models/utils.py +0 -0
  125. {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  126. {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/migrations/0002_test_field_removed.py +0 -0
  127. {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  128. {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/examples/migrations/__init__.py +0 -0
  129. {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/settings.py +0 -0
  130. {plain_models-0.42.0 → plain_models-0.43.0}/tests/app/urls.py +0 -0
  131. {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_database_url.py +0 -0
  132. {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_delete_behaviors.py +0 -0
  133. {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_exceptions.py +0 -0
  134. {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_manager_assignment.py +0 -0
  135. {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_models.py +0 -0
  136. {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_related_descriptors.py +0 -0
  137. {plain_models-0.42.0 → plain_models-0.43.0}/tests/test_related_manager_api.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.42.0
3
+ Version: 0.43.0
4
4
  Summary: Model your data and store it in a database.
5
5
  Author-email: Dave Gaeddert <dave.gaeddert@dropseed.dev>
6
6
  License-File: LICENSE
@@ -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
- ManyToManyDescriptor,
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 (is_valid_id or related_name.endswith("+")):
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, ManyToManyDescriptor(self.remote_field, reverse=False))
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
- ManyToManyDescriptor(self.remote_field, reverse=True),
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
+ )