plain.models 0.41.1__tar.gz → 0.42.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.41.1 → plain_models-0.42.0}/PKG-INFO +66 -23
  2. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/CHANGELOG.md +17 -0
  3. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/README.md +65 -22
  4. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/__init__.py +0 -2
  5. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/base.py +12 -34
  6. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/cli.py +16 -22
  7. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/constraints.py +1 -1
  8. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/deletion.py +1 -1
  9. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/fields/__init__.py +1 -1
  10. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/fields/related.py +1 -4
  11. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/fields/related_descriptors.py +57 -53
  12. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/fields/related_lookups.py +2 -2
  13. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/fields/reverse_related.py +1 -1
  14. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/forms.py +1 -1
  15. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/autodetector.py +0 -18
  16. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/operations/__init__.py +0 -2
  17. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/operations/models.py +3 -57
  18. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/operations/special.py +2 -8
  19. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/recorder.py +1 -1
  20. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/serializer.py +0 -12
  21. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/state.py +1 -55
  22. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/options.py +23 -86
  23. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/query.py +10 -41
  24. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/query_utils.py +1 -1
  25. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/sql/compiler.py +5 -5
  26. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/sql/query.py +2 -2
  27. {plain_models-0.41.1 → plain_models-0.42.0}/pyproject.toml +1 -1
  28. {plain_models-0.41.1 → plain_models-0.42.0}/tests/app/examples/migrations/0002_test_field_removed.py +1 -1
  29. {plain_models-0.41.1 → plain_models-0.42.0}/tests/app/examples/models.py +39 -1
  30. {plain_models-0.41.1 → plain_models-0.42.0}/tests/test_delete_behaviors.py +15 -15
  31. plain_models-0.42.0/tests/test_exceptions.py +63 -0
  32. plain_models-0.42.0/tests/test_manager_assignment.py +121 -0
  33. plain_models-0.42.0/tests/test_models.py +25 -0
  34. plain_models-0.42.0/tests/test_related_descriptors.py +236 -0
  35. plain_models-0.42.0/tests/test_related_manager_api.py +153 -0
  36. plain_models-0.41.1/plain/models/manager.py +0 -176
  37. plain_models-0.41.1/tests/test_models.py +0 -25
  38. {plain_models-0.41.1 → plain_models-0.42.0}/.gitignore +0 -0
  39. {plain_models-0.41.1 → plain_models-0.42.0}/LICENSE +0 -0
  40. {plain_models-0.41.1 → plain_models-0.42.0}/README.md +0 -0
  41. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/AGENTS.md +0 -0
  42. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/aggregates.py +0 -0
  43. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/__init__.py +0 -0
  44. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/__init__.py +0 -0
  45. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/base.py +0 -0
  46. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/client.py +0 -0
  47. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/creation.py +0 -0
  48. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/features.py +0 -0
  49. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/introspection.py +0 -0
  50. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/operations.py +0 -0
  51. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/schema.py +0 -0
  52. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/base/validation.py +0 -0
  53. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/ddl_references.py +0 -0
  54. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/__init__.py +0 -0
  55. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/base.py +0 -0
  56. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/client.py +0 -0
  57. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/compiler.py +0 -0
  58. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/creation.py +0 -0
  59. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/features.py +0 -0
  60. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/introspection.py +0 -0
  61. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/operations.py +0 -0
  62. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/schema.py +0 -0
  63. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/mysql/validation.py +0 -0
  64. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/postgresql/__init__.py +0 -0
  65. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/postgresql/base.py +0 -0
  66. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/postgresql/client.py +0 -0
  67. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/postgresql/creation.py +0 -0
  68. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/postgresql/features.py +0 -0
  69. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/postgresql/introspection.py +0 -0
  70. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/postgresql/operations.py +0 -0
  71. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/postgresql/schema.py +0 -0
  72. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/__init__.py +0 -0
  73. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/_functions.py +0 -0
  74. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/base.py +0 -0
  75. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/client.py +0 -0
  76. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/creation.py +0 -0
  77. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/features.py +0 -0
  78. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/introspection.py +0 -0
  79. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/operations.py +0 -0
  80. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/sqlite3/schema.py +0 -0
  81. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backends/utils.py +0 -0
  82. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backups/__init__.py +0 -0
  83. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backups/cli.py +0 -0
  84. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backups/clients.py +0 -0
  85. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/backups/core.py +0 -0
  86. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/config.py +0 -0
  87. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/connections.py +0 -0
  88. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/constants.py +0 -0
  89. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/database_url.py +0 -0
  90. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/db.py +0 -0
  91. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/default_settings.py +0 -0
  92. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/entrypoints.py +0 -0
  93. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/enums.py +0 -0
  94. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/exceptions.py +0 -0
  95. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/expressions.py +0 -0
  96. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/fields/json.py +0 -0
  97. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/fields/mixins.py +0 -0
  98. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/functions/__init__.py +0 -0
  99. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/functions/comparison.py +0 -0
  100. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/functions/datetime.py +0 -0
  101. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/functions/math.py +0 -0
  102. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/functions/mixins.py +0 -0
  103. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/functions/text.py +0 -0
  104. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/functions/window.py +0 -0
  105. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/indexes.py +0 -0
  106. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/lookups.py +0 -0
  107. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/__init__.py +0 -0
  108. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/exceptions.py +0 -0
  109. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/executor.py +0 -0
  110. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/graph.py +0 -0
  111. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/loader.py +0 -0
  112. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/migration.py +0 -0
  113. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/operations/base.py +0 -0
  114. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/operations/fields.py +0 -0
  115. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/optimizer.py +0 -0
  116. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/questioner.py +0 -0
  117. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/utils.py +0 -0
  118. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/migrations/writer.py +0 -0
  119. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/otel.py +0 -0
  120. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/preflight.py +0 -0
  121. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/registry.py +0 -0
  122. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/sql/__init__.py +0 -0
  123. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/sql/constants.py +0 -0
  124. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/sql/datastructures.py +0 -0
  125. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/sql/subqueries.py +0 -0
  126. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/sql/where.py +0 -0
  127. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/test/__init__.py +0 -0
  128. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/test/pytest.py +0 -0
  129. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/test/utils.py +0 -0
  130. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/transaction.py +0 -0
  131. {plain_models-0.41.1 → plain_models-0.42.0}/plain/models/utils.py +0 -0
  132. {plain_models-0.41.1 → plain_models-0.42.0}/tests/app/examples/migrations/0001_initial.py +0 -0
  133. {plain_models-0.41.1 → plain_models-0.42.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
  134. {plain_models-0.41.1 → plain_models-0.42.0}/tests/app/examples/migrations/__init__.py +0 -0
  135. {plain_models-0.41.1 → plain_models-0.42.0}/tests/app/settings.py +0 -0
  136. {plain_models-0.41.1 → plain_models-0.42.0}/tests/app/urls.py +0 -0
  137. {plain_models-0.41.1 → plain_models-0.42.0}/tests/test_database_url.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: plain.models
3
- Version: 0.41.1
3
+ Version: 0.42.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
@@ -20,7 +20,7 @@ Description-Content-Type: text/markdown
20
20
  - [Fields](#fields)
21
21
  - [Validation](#validation)
22
22
  - [Indexes and constraints](#indexes-and-constraints)
23
- - [Managers](#managers)
23
+ - [Custom QuerySets](#custom-querysets)
24
24
  - [Forms](#forms)
25
25
  - [Sharing fields across models](#sharing-fields-across-models)
26
26
  - [Installation](#installation)
@@ -54,7 +54,7 @@ from .models import User
54
54
 
55
55
 
56
56
  # Create a new user
57
- user = User.objects.create(
57
+ user = User.query.create(
58
58
  email="test@example.com",
59
59
  password="password",
60
60
  )
@@ -67,7 +67,7 @@ user.save()
67
67
  user.delete()
68
68
 
69
69
  # Query for users
70
- admin_users = User.objects.filter(is_admin=True)
70
+ admin_users = User.query.filter(is_admin=True)
71
71
  ```
72
72
 
73
73
  ## Database connection
@@ -96,30 +96,30 @@ Multiple backends are supported, including Postgres, MySQL, and SQLite.
96
96
 
97
97
  ## Querying
98
98
 
99
- Models come with a powerful query API through their [`Manager`](./manager.py#Manager) interface:
99
+ Models come with a powerful query API through their [`QuerySet`](./query.py#QuerySet) interface:
100
100
 
101
101
  ```python
102
102
  # Get all users
103
- all_users = User.objects.all()
103
+ all_users = User.query.all()
104
104
 
105
105
  # Filter users
106
- admin_users = User.objects.filter(is_admin=True)
107
- recent_users = User.objects.filter(created_at__gte=datetime.now() - timedelta(days=7))
106
+ admin_users = User.query.filter(is_admin=True)
107
+ recent_users = User.query.filter(created_at__gte=datetime.now() - timedelta(days=7))
108
108
 
109
109
  # Get a single user
110
- user = User.objects.get(email="test@example.com")
110
+ user = User.query.get(email="test@example.com")
111
111
 
112
112
  # Complex queries with Q objects
113
113
  from plain.models import Q
114
- users = User.objects.filter(
114
+ users = User.query.filter(
115
115
  Q(is_admin=True) | Q(email__endswith="@example.com")
116
116
  )
117
117
 
118
118
  # Ordering
119
- users = User.objects.order_by("-created_at")
119
+ users = User.query.order_by("-created_at")
120
120
 
121
121
  # Limiting results
122
- first_10_users = User.objects.all()[:10]
122
+ first_10_users = User.query.all()[:10]
123
123
  ```
124
124
 
125
125
  For more advanced querying options, see the [`QuerySet`](./query.py#QuerySet) class.
@@ -222,28 +222,71 @@ class User(models.Model):
222
222
  ]
223
223
  ```
224
224
 
225
- ## Managers
225
+ ## Custom QuerySets
226
226
 
227
- [`Manager`](./manager.py#Manager) objects provide the interface for querying models:
227
+ With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods. There are several ways to use custom QuerySets:
228
+
229
+ ### Setting a default QuerySet for a model
230
+
231
+ Use `Meta.queryset_class` to set a custom QuerySet that will be used by `Model.query`:
232
+
233
+ ```python
234
+ class PublishedQuerySet(models.QuerySet):
235
+ def published_only(self):
236
+ return self.filter(status="published")
237
+
238
+ def draft_only(self):
239
+ return self.filter(status="draft")
240
+
241
+ @models.register_model
242
+ class Article(models.Model):
243
+ title = models.CharField(max_length=200)
244
+ status = models.CharField(max_length=20)
245
+
246
+ class Meta:
247
+ queryset_class = PublishedQuerySet
248
+
249
+ # Usage - all methods available on Article.objects
250
+ all_articles = Article.query.all()
251
+ published_articles = Article.query.published_only()
252
+ draft_articles = Article.query.draft_only()
253
+ ```
254
+
255
+ ### Using custom QuerySets without formal attachment
256
+
257
+ You can also use custom QuerySets manually without setting them as the default:
228
258
 
229
259
  ```python
230
- class PublishedManager(models.Manager):
231
- def get_queryset(self):
232
- return super().get_queryset().filter(status="published")
260
+ class SpecialQuerySet(models.QuerySet):
261
+ def special_filter(self):
262
+ return self.filter(special=True)
233
263
 
264
+ # Create and use the QuerySet manually
265
+ special_qs = SpecialQuerySet(model=Article)
266
+ special_articles = special_qs.special_filter()
267
+ ```
268
+
269
+ ### Using classmethods for convenience
270
+
271
+ For even cleaner API, add classmethods to your model:
272
+
273
+ ```python
274
+ @models.register_model
234
275
  class Article(models.Model):
235
276
  title = models.CharField(max_length=200)
236
277
  status = models.CharField(max_length=20)
237
278
 
238
- # Default manager
239
- objects = models.Manager()
279
+ @classmethod
280
+ def published(cls):
281
+ return PublishedQuerySet(model=cls).published_only()
240
282
 
241
- # Custom manager
242
- published = PublishedManager()
283
+ @classmethod
284
+ def drafts(cls):
285
+ return PublishedQuerySet(model=cls).draft_only()
243
286
 
244
287
  # Usage
245
- all_articles = Article.objects.all()
246
- published_articles = Article.published.all()
288
+ published_articles = Article.published()
289
+ draft_articles = Article.drafts()
247
290
  ```
248
291
 
249
292
  ## Forms
@@ -1,5 +1,22 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.42.0](https://github.com/dropseed/plain/releases/plain-models@0.42.0) (2025-09-12)
4
+
5
+ ### What's changed
6
+
7
+ - The model manager interface has been renamed from `.objects` to `.query` ([037a239](https://github.com/dropseed/plain/commit/037a239ef4))
8
+ - Manager functionality has been merged into QuerySet, simplifying the architecture - custom QuerySets can now be set directly via `Meta.queryset_class` ([bbaee93](https://github.com/dropseed/plain/commit/bbaee93839))
9
+ - The `objects` manager is now set directly on the Model class for better type checking ([fccc5be](https://github.com/dropseed/plain/commit/fccc5be13e))
10
+ - Database backups are now created automatically during migrations when in DEBUG mode ([c8023074](https://github.com/dropseed/plain/commit/c8023074e9))
11
+ - Removed several legacy manager features: `default_related_name`, `base_manager_name`, `creation_counter`, `use_in_migrations`, `auto_created`, and routing hints ([multiple commits](https://github.com/dropseed/plain/compare/plain-models@0.41.1...037a239ef4))
12
+
13
+ ### Upgrade instructions
14
+
15
+ - Replace all usage of `Model.objects` with `Model.query` in your codebase (e.g., `User.objects.filter()` becomes `User.query.filter()`)
16
+ - If you have custom managers, convert them to custom QuerySets and set them using `Meta.queryset_class` instead of assigning to class attributes (if there is more than one custom manager on a class, invoke the new QuerySet class directly or add a shortcut on the Model using `@classmethod`)
17
+ - Remove any usage of the removed manager features: `default_related_name`, `base_manager_name`, manager `creation_counter`, `use_in_migrations`, `auto_created`, and database routing hints
18
+ - Any reverse accessors (typically `<related_model>_set` or defined by `related_name`) will now return a manager class for the additional `add()`, `remove()`, `clear()`, etc. methods and the regular queryset methods will be available via `.query` (e.g., `user.articles.first()` becomes `user.articles.query.first()`)
19
+
3
20
  ## [0.41.1](https://github.com/dropseed/plain/releases/plain-models@0.41.1) (2025-09-09)
4
21
 
5
22
  ### What's changed
@@ -9,7 +9,7 @@
9
9
  - [Fields](#fields)
10
10
  - [Validation](#validation)
11
11
  - [Indexes and constraints](#indexes-and-constraints)
12
- - [Managers](#managers)
12
+ - [Custom QuerySets](#custom-querysets)
13
13
  - [Forms](#forms)
14
14
  - [Sharing fields across models](#sharing-fields-across-models)
15
15
  - [Installation](#installation)
@@ -43,7 +43,7 @@ from .models import User
43
43
 
44
44
 
45
45
  # Create a new user
46
- user = User.objects.create(
46
+ user = User.query.create(
47
47
  email="test@example.com",
48
48
  password="password",
49
49
  )
@@ -56,7 +56,7 @@ user.save()
56
56
  user.delete()
57
57
 
58
58
  # Query for users
59
- admin_users = User.objects.filter(is_admin=True)
59
+ admin_users = User.query.filter(is_admin=True)
60
60
  ```
61
61
 
62
62
  ## Database connection
@@ -85,30 +85,30 @@ Multiple backends are supported, including Postgres, MySQL, and SQLite.
85
85
 
86
86
  ## Querying
87
87
 
88
- Models come with a powerful query API through their [`Manager`](./manager.py#Manager) interface:
88
+ Models come with a powerful query API through their [`QuerySet`](./query.py#QuerySet) interface:
89
89
 
90
90
  ```python
91
91
  # Get all users
92
- all_users = User.objects.all()
92
+ all_users = User.query.all()
93
93
 
94
94
  # Filter users
95
- admin_users = User.objects.filter(is_admin=True)
96
- recent_users = User.objects.filter(created_at__gte=datetime.now() - timedelta(days=7))
95
+ admin_users = User.query.filter(is_admin=True)
96
+ recent_users = User.query.filter(created_at__gte=datetime.now() - timedelta(days=7))
97
97
 
98
98
  # Get a single user
99
- user = User.objects.get(email="test@example.com")
99
+ user = User.query.get(email="test@example.com")
100
100
 
101
101
  # Complex queries with Q objects
102
102
  from plain.models import Q
103
- users = User.objects.filter(
103
+ users = User.query.filter(
104
104
  Q(is_admin=True) | Q(email__endswith="@example.com")
105
105
  )
106
106
 
107
107
  # Ordering
108
- users = User.objects.order_by("-created_at")
108
+ users = User.query.order_by("-created_at")
109
109
 
110
110
  # Limiting results
111
- first_10_users = User.objects.all()[:10]
111
+ first_10_users = User.query.all()[:10]
112
112
  ```
113
113
 
114
114
  For more advanced querying options, see the [`QuerySet`](./query.py#QuerySet) class.
@@ -211,28 +211,71 @@ class User(models.Model):
211
211
  ]
212
212
  ```
213
213
 
214
- ## Managers
214
+ ## Custom QuerySets
215
215
 
216
- [`Manager`](./manager.py#Manager) objects provide the interface for querying models:
216
+ With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods. There are several ways to use custom QuerySets:
217
+
218
+ ### Setting a default QuerySet for a model
219
+
220
+ Use `Meta.queryset_class` to set a custom QuerySet that will be used by `Model.query`:
221
+
222
+ ```python
223
+ class PublishedQuerySet(models.QuerySet):
224
+ def published_only(self):
225
+ return self.filter(status="published")
226
+
227
+ def draft_only(self):
228
+ return self.filter(status="draft")
229
+
230
+ @models.register_model
231
+ class Article(models.Model):
232
+ title = models.CharField(max_length=200)
233
+ status = models.CharField(max_length=20)
234
+
235
+ class Meta:
236
+ queryset_class = PublishedQuerySet
237
+
238
+ # Usage - all methods available on Article.objects
239
+ all_articles = Article.query.all()
240
+ published_articles = Article.query.published_only()
241
+ draft_articles = Article.query.draft_only()
242
+ ```
243
+
244
+ ### Using custom QuerySets without formal attachment
245
+
246
+ You can also use custom QuerySets manually without setting them as the default:
217
247
 
218
248
  ```python
219
- class PublishedManager(models.Manager):
220
- def get_queryset(self):
221
- return super().get_queryset().filter(status="published")
249
+ class SpecialQuerySet(models.QuerySet):
250
+ def special_filter(self):
251
+ return self.filter(special=True)
222
252
 
253
+ # Create and use the QuerySet manually
254
+ special_qs = SpecialQuerySet(model=Article)
255
+ special_articles = special_qs.special_filter()
256
+ ```
257
+
258
+ ### Using classmethods for convenience
259
+
260
+ For even cleaner API, add classmethods to your model:
261
+
262
+ ```python
263
+ @models.register_model
223
264
  class Article(models.Model):
224
265
  title = models.CharField(max_length=200)
225
266
  status = models.CharField(max_length=20)
226
267
 
227
- # Default manager
228
- objects = models.Manager()
268
+ @classmethod
269
+ def published(cls):
270
+ return PublishedQuerySet(model=cls).published_only()
229
271
 
230
- # Custom manager
231
- published = PublishedManager()
272
+ @classmethod
273
+ def drafts(cls):
274
+ return PublishedQuerySet(model=cls).draft_only()
232
275
 
233
276
  # Usage
234
- all_articles = Article.objects.all()
235
- published_articles = Article.published.all()
277
+ published_articles = Article.published()
278
+ draft_articles = Article.drafts()
236
279
  ```
237
280
 
238
281
  ## Forms
@@ -59,7 +59,6 @@ from .fields.json import JSONField
59
59
  from .indexes import * # NOQA
60
60
  from .indexes import __all__ as indexes_all
61
61
  from .lookups import Lookup, Transform
62
- from .manager import Manager
63
62
  from .query import Prefetch, QuerySet, prefetch_related_objects
64
63
  from .query_utils import FilteredRelation, Q
65
64
  from .registry import models_registry, register_model
@@ -108,7 +107,6 @@ __all__ += [
108
107
  "JSONField",
109
108
  "Lookup",
110
109
  "Transform",
111
- "Manager",
112
110
  "Prefetch",
113
111
  "Q",
114
112
  "QuerySet",
@@ -24,7 +24,6 @@ from plain.models.deletion import Collector
24
24
  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
- from plain.models.manager import Manager
28
27
  from plain.models.options import Options
29
28
  from plain.models.query import F, Q
30
29
  from plain.packages import packages_registry
@@ -75,7 +74,7 @@ class ModelBase(type):
75
74
  new_class._add_exceptions()
76
75
 
77
76
  # Now go back over all the attrs on this class see if they have a contribute_to_class() method.
78
- # Attributes with contribute_to_class are fields, meta options, and managers.
77
+ # Attributes with contribute_to_class are fields and meta options.
79
78
  for attr_name, attr_value in inspect.getmembers(new_class):
80
79
  if attr_name.startswith("_"):
81
80
  continue
@@ -161,16 +160,6 @@ class ModelBase(type):
161
160
  ", ".join(f.name for f in opts.fields),
162
161
  )
163
162
 
164
- if not opts.managers:
165
- if any(f.name == "objects" for f in opts.fields):
166
- raise ValueError(
167
- f"Model {cls.__name__} must specify a custom Manager, because it has a "
168
- "field named 'objects'."
169
- )
170
- manager = Manager()
171
- manager.auto_created = True
172
- cls.add_to_class("objects", manager)
173
-
174
163
  # Set the name of _meta.indexes. This can't be done in
175
164
  # Options.contribute_to_class() because fields haven't been added to
176
165
  # the model at that point.
@@ -179,12 +168,8 @@ class ModelBase(type):
179
168
  index.set_name_with_model(cls)
180
169
 
181
170
  @property
182
- def _base_manager(cls):
183
- return cls._meta.base_manager
184
-
185
- @property
186
- def _default_manager(cls):
187
- return cls._meta.default_manager
171
+ def query(cls):
172
+ return cls._meta.queryset
188
173
 
189
174
 
190
175
  class ModelStateFieldsCacheDescriptor:
@@ -207,6 +192,9 @@ class ModelState:
207
192
 
208
193
 
209
194
  class Model(metaclass=ModelBase):
195
+ DoesNotExist: type[ObjectDoesNotExist]
196
+ MultipleObjectsReturned: type[MultipleObjectsReturned]
197
+
210
198
  def __init__(self, *args, **kwargs):
211
199
  # Alias some things as locals to avoid repeat global lookups
212
200
  cls = self.__class__
@@ -434,7 +422,7 @@ class Model(metaclass=ModelBase):
434
422
  "are not allowed in fields."
435
423
  )
436
424
 
437
- db_instance_qs = self.__class__._base_manager.get_queryset().filter(id=self.id)
425
+ db_instance_qs = self.__class__._meta.base_queryset.filter(id=self.id)
438
426
 
439
427
  # Use provided fields, if not set then reload all non-deferred fields.
440
428
  deferred_fields = self.get_deferred_fields()
@@ -617,7 +605,7 @@ class Model(metaclass=ModelBase):
617
605
  force_insert = True
618
606
  # If possible, try an UPDATE. If that doesn't update anything, do an INSERT.
619
607
  if id_set and not force_insert:
620
- base_qs = cls._base_manager
608
+ base_qs = meta.base_queryset
621
609
  values = [
622
610
  (
623
611
  f,
@@ -641,7 +629,7 @@ class Model(metaclass=ModelBase):
641
629
  fields = [f for f in fields if f is not id_field]
642
630
 
643
631
  returning_fields = meta.db_returning_fields
644
- results = self._do_insert(cls._base_manager, fields, returning_fields, raw)
632
+ results = self._do_insert(meta.base_queryset, fields, returning_fields, raw)
645
633
  if results:
646
634
  for value, field in zip(results[0], returning_fields):
647
635
  setattr(self, field.attname, value)
@@ -738,7 +726,7 @@ class Model(metaclass=ModelBase):
738
726
  q = Q.create([(field.name, param), (f"id__{op}", self.id)], connector=Q.AND)
739
727
  q = Q.create([q, (f"{field.name}__{op}", param)], connector=Q.OR)
740
728
  qs = (
741
- self.__class__._default_manager.filter(**kwargs)
729
+ self.__class__.query.filter(**kwargs)
742
730
  .filter(q)
743
731
  .order_by(f"{order}{field.name}", f"{order}id")
744
732
  )
@@ -836,7 +824,7 @@ class Model(metaclass=ModelBase):
836
824
  if len(unique_check) != len(lookup_kwargs):
837
825
  continue
838
826
 
839
- qs = model_class._default_manager.filter(**lookup_kwargs)
827
+ qs = model_class.query.filter(**lookup_kwargs)
840
828
 
841
829
  # Exclude the current object from the query if we are editing an
842
830
  # instance (as opposed to creating a new one)
@@ -995,9 +983,7 @@ class Model(metaclass=ModelBase):
995
983
 
996
984
  @classmethod
997
985
  def check(cls, **kwargs):
998
- errors = [
999
- *cls._check_managers(**kwargs),
1000
- ]
986
+ errors = []
1001
987
 
1002
988
  database = kwargs.get("database", False)
1003
989
  errors += [
@@ -1045,14 +1031,6 @@ class Model(metaclass=ModelBase):
1045
1031
  )
1046
1032
  return errors
1047
1033
 
1048
- @classmethod
1049
- def _check_managers(cls, **kwargs):
1050
- """Perform all manager checks."""
1051
- errors = []
1052
- for manager in cls._meta.managers:
1053
- errors.extend(manager.check(**kwargs))
1054
- return errors
1055
-
1056
1034
  @classmethod
1057
1035
  def _check_fields(cls, **kwargs):
1058
1036
  """Perform all field checks."""
@@ -479,7 +479,7 @@ def migrate(
479
479
  "Migrations can be pruned only when a package is specified."
480
480
  )
481
481
  if verbosity > 0:
482
- click.echo("Pruning migrations:", color="cyan")
482
+ click.secho("Pruning migrations:", fg="cyan")
483
483
  to_prune = set(executor.loader.applied_migrations) - set(
484
484
  executor.loader.disk_migrations
485
485
  )
@@ -529,16 +529,16 @@ def migrate(
529
529
  migration_plan = executor.migration_plan(targets)
530
530
 
531
531
  if plan:
532
- click.echo("Planned operations:", color="cyan")
532
+ click.secho("Planned operations:", fg="cyan")
533
533
  if not migration_plan:
534
534
  click.echo(" No planned migration operations.")
535
535
  else:
536
536
  for migration in migration_plan:
537
- click.echo(str(migration), color="cyan")
537
+ click.secho(str(migration), fg="cyan")
538
538
  for operation in migration.operations:
539
539
  message, is_error = describe_operation(operation)
540
540
  if is_error:
541
- click.echo(" " + message, fg="yellow")
541
+ click.secho(" " + message, fg="yellow")
542
542
  else:
543
543
  click.echo(" " + message)
544
544
  if check_unapplied:
@@ -555,18 +555,18 @@ def migrate(
555
555
 
556
556
  # Print some useful info
557
557
  if verbosity >= 1:
558
- click.echo("Operations to perform:", color="cyan")
558
+ click.secho("Operations to perform:", fg="cyan")
559
559
 
560
560
  if target_package_labels_only:
561
- click.echo(
561
+ click.secho(
562
562
  " Apply all migrations: "
563
563
  + (", ".join(sorted({a for a, n in targets})) or "(none)"),
564
- color="yellow",
564
+ fg="yellow",
565
565
  )
566
566
  else:
567
- click.echo(
567
+ click.secho(
568
568
  f" Target specific migration: {targets[0][1]}, from {targets[0][0]}",
569
- color="yellow",
569
+ fg="yellow",
570
570
  )
571
571
 
572
572
  pre_migrate_state = executor._create_project_state(with_applied_migrations=True)
@@ -575,30 +575,24 @@ def migrate(
575
575
  # pprint(sql)
576
576
 
577
577
  if migration_plan:
578
- if backup or (
579
- backup is None
580
- and settings.DEBUG
581
- and (
582
- no_input
583
- or click.confirm(
584
- "\nYou are in DEBUG mode. Would you like to make a database backup before running migrations?",
585
- default=True,
586
- )
587
- )
588
- ):
578
+ if backup or (backup is None and settings.DEBUG):
589
579
  backup_name = f"migrate_{time.strftime('%Y%m%d_%H%M%S')}"
580
+ click.secho(
581
+ f"Creating backup before applying migrations: {backup_name}",
582
+ bold=True,
583
+ )
590
584
  # Can't use ctx.invoke because this is called by the test db creation currently,
591
585
  # which doesn't have a context.
592
586
  create_backup.callback(
593
587
  backup_name=backup_name,
594
588
  pg_dump=os.environ.get(
595
589
  "PG_DUMP", "pg_dump"
596
- ), # Have to this again manually
590
+ ), # Have to pass this in manually
597
591
  )
598
592
  print()
599
593
 
600
594
  if verbosity >= 1:
601
- click.echo("Running migrations:", color="cyan")
595
+ click.secho("Running migrations:", fg="cyan")
602
596
 
603
597
  post_migrate_state = executor.migrate(
604
598
  targets,
@@ -347,7 +347,7 @@ class UniqueConstraint(BaseConstraint):
347
347
  return path, self.expressions, kwargs
348
348
 
349
349
  def validate(self, model, instance, exclude=None):
350
- queryset = model._default_manager
350
+ queryset = model.query
351
351
  if self.fields:
352
352
  lookup_kwargs = {}
353
353
  for field_name in self.fields:
@@ -352,7 +352,7 @@ class Collector:
352
352
  [(f"{related_field.name}__in", objs) for related_field in related_fields],
353
353
  connector=query_utils.Q.OR,
354
354
  )
355
- return related_model._base_manager.filter(predicate)
355
+ return related_model._meta.base_queryset.filter(predicate)
356
356
 
357
357
  def sort(self):
358
358
  sorted_models = []
@@ -901,7 +901,7 @@ class Field(RegisterLookupMixin):
901
901
  if hasattr(self.remote_field, "get_related_field")
902
902
  else "id"
903
903
  )
904
- qs = rel_model._default_manager.complex_filter(limit_choices_to)
904
+ qs = rel_model.query.complex_filter(limit_choices_to)
905
905
  if ordering:
906
906
  qs = qs.order_by(*ordering)
907
907
  return (blank_choice if include_blank else []) + [
@@ -310,9 +310,6 @@ class RelatedField(FieldCacheMixin, Field):
310
310
 
311
311
  if self.remote_field.related_name:
312
312
  related_name = self.remote_field.related_name
313
- else:
314
- related_name = self.opts.default_related_name
315
- if related_name:
316
313
  related_name %= {
317
314
  "class": cls.__name__.lower(),
318
315
  "model_name": cls._meta.model_name.lower(),
@@ -672,7 +669,7 @@ class ForeignKey(RelatedField):
672
669
  if value is None:
673
670
  return
674
671
 
675
- qs = self.remote_field.model._base_manager.filter(
672
+ qs = self.remote_field.model._meta.base_queryset.filter(
676
673
  **{self.remote_field.field_name: value}
677
674
  )
678
675
  qs = qs.complex_filter(self.get_limit_choices_to())