plain.models 0.41.1__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.41.1 → plain_models-0.43.0}/PKG-INFO +66 -23
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/CHANGELOG.md +32 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/README.md +65 -22
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/__init__.py +0 -2
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/base.py +14 -35
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/cli.py +16 -22
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/constraints.py +1 -1
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/deletion.py +1 -1
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/fields/__init__.py +1 -1
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/fields/related.py +7 -32
- plain_models-0.43.0/plain/models/fields/related_descriptors.py +393 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/fields/related_lookups.py +2 -2
- plain_models-0.43.0/plain/models/fields/related_managers.py +629 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/fields/reverse_related.py +5 -8
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/forms.py +1 -1
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/autodetector.py +0 -18
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/operations/__init__.py +0 -2
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/operations/models.py +3 -57
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/operations/special.py +2 -8
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/recorder.py +1 -1
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/serializer.py +0 -12
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/state.py +1 -55
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/options.py +23 -86
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/query.py +10 -41
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/query_utils.py +1 -1
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/sql/compiler.py +5 -5
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/sql/query.py +2 -2
- {plain_models-0.41.1 → plain_models-0.43.0}/pyproject.toml +1 -1
- {plain_models-0.41.1 → plain_models-0.43.0}/tests/app/examples/migrations/0002_test_field_removed.py +1 -1
- {plain_models-0.41.1 → plain_models-0.43.0}/tests/app/examples/models.py +42 -2
- {plain_models-0.41.1 → plain_models-0.43.0}/tests/test_delete_behaviors.py +15 -15
- plain_models-0.43.0/tests/test_exceptions.py +63 -0
- plain_models-0.43.0/tests/test_manager_assignment.py +121 -0
- plain_models-0.43.0/tests/test_models.py +25 -0
- plain_models-0.43.0/tests/test_related_descriptors.py +236 -0
- plain_models-0.43.0/tests/test_related_manager_api.py +153 -0
- plain_models-0.41.1/plain/models/fields/related_descriptors.py +0 -943
- plain_models-0.41.1/plain/models/manager.py +0 -176
- plain_models-0.41.1/tests/test_models.py +0 -25
- {plain_models-0.41.1 → plain_models-0.43.0}/.gitignore +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/LICENSE +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/README.md +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/AGENTS.md +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/aggregates.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/base.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/client.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/creation.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/features.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/introspection.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/operations.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/schema.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/base/validation.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/ddl_references.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/base.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/client.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/compiler.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/creation.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/features.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/introspection.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/operations.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/schema.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/mysql/validation.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/postgresql/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/postgresql/base.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/postgresql/client.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/postgresql/creation.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/postgresql/features.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/postgresql/introspection.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/postgresql/operations.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/postgresql/schema.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/_functions.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/base.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/client.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/creation.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/features.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/introspection.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/operations.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/sqlite3/schema.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backends/utils.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backups/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backups/cli.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backups/clients.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/backups/core.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/config.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/connections.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/constants.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/database_url.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/db.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/default_settings.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/entrypoints.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/enums.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/exceptions.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/expressions.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/fields/json.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/fields/mixins.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/functions/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/functions/comparison.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/functions/datetime.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/functions/math.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/functions/mixins.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/functions/text.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/functions/window.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/indexes.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/lookups.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/exceptions.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/executor.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/graph.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/loader.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/migration.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/operations/base.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/operations/fields.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/optimizer.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/questioner.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/utils.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/migrations/writer.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/otel.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/preflight.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/registry.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/sql/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/sql/constants.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/sql/datastructures.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/sql/subqueries.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/sql/where.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/test/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/test/pytest.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/test/utils.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/transaction.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/plain/models/utils.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/tests/app/examples/migrations/0001_initial.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/tests/app/examples/migrations/0003_deleteparent_childsetnull_childsetdefault_and_more.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/tests/app/examples/migrations/__init__.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/tests/app/settings.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.0}/tests/app/urls.py +0 -0
- {plain_models-0.41.1 → plain_models-0.43.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.
|
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
|
@@ -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
|
-
- [
|
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.
|
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.
|
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 [`
|
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.
|
103
|
+
all_users = User.query.all()
|
104
104
|
|
105
105
|
# Filter users
|
106
|
-
admin_users = User.
|
107
|
-
recent_users = User.
|
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.
|
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.
|
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.
|
119
|
+
users = User.query.order_by("-created_at")
|
120
120
|
|
121
121
|
# Limiting results
|
122
|
-
first_10_users = User.
|
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
|
-
##
|
225
|
+
## Custom QuerySets
|
226
226
|
|
227
|
-
[`
|
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
|
231
|
-
def
|
232
|
-
return
|
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
|
-
|
239
|
-
|
279
|
+
@classmethod
|
280
|
+
def published(cls):
|
281
|
+
return PublishedQuerySet(model=cls).published_only()
|
240
282
|
|
241
|
-
|
242
|
-
|
283
|
+
@classmethod
|
284
|
+
def drafts(cls):
|
285
|
+
return PublishedQuerySet(model=cls).draft_only()
|
243
286
|
|
244
287
|
# Usage
|
245
|
-
|
246
|
-
|
288
|
+
published_articles = Article.published()
|
289
|
+
draft_articles = Article.drafts()
|
247
290
|
```
|
248
291
|
|
249
292
|
## Forms
|
@@ -1,5 +1,37 @@
|
|
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
|
+
|
18
|
+
## [0.42.0](https://github.com/dropseed/plain/releases/plain-models@0.42.0) (2025-09-12)
|
19
|
+
|
20
|
+
### What's changed
|
21
|
+
|
22
|
+
- The model manager interface has been renamed from `.objects` to `.query` ([037a239](https://github.com/dropseed/plain/commit/037a239ef4))
|
23
|
+
- 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))
|
24
|
+
- The `objects` manager is now set directly on the Model class for better type checking ([fccc5be](https://github.com/dropseed/plain/commit/fccc5be13e))
|
25
|
+
- Database backups are now created automatically during migrations when in DEBUG mode ([c8023074](https://github.com/dropseed/plain/commit/c8023074e9))
|
26
|
+
- 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))
|
27
|
+
|
28
|
+
### Upgrade instructions
|
29
|
+
|
30
|
+
- Replace all usage of `Model.objects` with `Model.query` in your codebase (e.g., `User.objects.filter()` becomes `User.query.filter()`)
|
31
|
+
- 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`)
|
32
|
+
- 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
|
33
|
+
- 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()`)
|
34
|
+
|
3
35
|
## [0.41.1](https://github.com/dropseed/plain/releases/plain-models@0.41.1) (2025-09-09)
|
4
36
|
|
5
37
|
### What's changed
|
@@ -9,7 +9,7 @@
|
|
9
9
|
- [Fields](#fields)
|
10
10
|
- [Validation](#validation)
|
11
11
|
- [Indexes and constraints](#indexes-and-constraints)
|
12
|
-
- [
|
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.
|
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.
|
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 [`
|
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.
|
92
|
+
all_users = User.query.all()
|
93
93
|
|
94
94
|
# Filter users
|
95
|
-
admin_users = User.
|
96
|
-
recent_users = User.
|
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.
|
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.
|
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.
|
108
|
+
users = User.query.order_by("-created_at")
|
109
109
|
|
110
110
|
# Limiting results
|
111
|
-
first_10_users = User.
|
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
|
-
##
|
214
|
+
## Custom QuerySets
|
215
215
|
|
216
|
-
[`
|
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
|
220
|
-
def
|
221
|
-
return
|
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
|
-
|
228
|
-
|
268
|
+
@classmethod
|
269
|
+
def published(cls):
|
270
|
+
return PublishedQuerySet(model=cls).published_only()
|
229
271
|
|
230
|
-
|
231
|
-
|
272
|
+
@classmethod
|
273
|
+
def drafts(cls):
|
274
|
+
return PublishedQuerySet(model=cls).draft_only()
|
232
275
|
|
233
276
|
# Usage
|
234
|
-
|
235
|
-
|
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,9 +24,8 @@ 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
|
-
from plain.models.query import F, Q
|
28
|
+
from plain.models.query import F, Q, QuerySet
|
30
29
|
from plain.packages import packages_registry
|
31
30
|
from plain.utils.encoding import force_str
|
32
31
|
from plain.utils.hashable import make_hashable
|
@@ -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
|
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,9 @@ class ModelBase(type):
|
|
179
168
|
index.set_name_with_model(cls)
|
180
169
|
|
181
170
|
@property
|
182
|
-
def
|
183
|
-
|
184
|
-
|
185
|
-
@property
|
186
|
-
def _default_manager(cls):
|
187
|
-
return cls._meta.default_manager
|
171
|
+
def query(cls) -> QuerySet:
|
172
|
+
"""Create a new QuerySet for this model."""
|
173
|
+
return cls._meta.queryset
|
188
174
|
|
189
175
|
|
190
176
|
class ModelStateFieldsCacheDescriptor:
|
@@ -207,6 +193,9 @@ class ModelState:
|
|
207
193
|
|
208
194
|
|
209
195
|
class Model(metaclass=ModelBase):
|
196
|
+
DoesNotExist: type[ObjectDoesNotExist]
|
197
|
+
MultipleObjectsReturned: type[MultipleObjectsReturned]
|
198
|
+
|
210
199
|
def __init__(self, *args, **kwargs):
|
211
200
|
# Alias some things as locals to avoid repeat global lookups
|
212
201
|
cls = self.__class__
|
@@ -434,7 +423,7 @@ class Model(metaclass=ModelBase):
|
|
434
423
|
"are not allowed in fields."
|
435
424
|
)
|
436
425
|
|
437
|
-
db_instance_qs = self.__class__.
|
426
|
+
db_instance_qs = self.__class__._meta.base_queryset.filter(id=self.id)
|
438
427
|
|
439
428
|
# Use provided fields, if not set then reload all non-deferred fields.
|
440
429
|
deferred_fields = self.get_deferred_fields()
|
@@ -617,7 +606,7 @@ class Model(metaclass=ModelBase):
|
|
617
606
|
force_insert = True
|
618
607
|
# If possible, try an UPDATE. If that doesn't update anything, do an INSERT.
|
619
608
|
if id_set and not force_insert:
|
620
|
-
base_qs =
|
609
|
+
base_qs = meta.base_queryset
|
621
610
|
values = [
|
622
611
|
(
|
623
612
|
f,
|
@@ -641,7 +630,7 @@ class Model(metaclass=ModelBase):
|
|
641
630
|
fields = [f for f in fields if f is not id_field]
|
642
631
|
|
643
632
|
returning_fields = meta.db_returning_fields
|
644
|
-
results = self._do_insert(
|
633
|
+
results = self._do_insert(meta.base_queryset, fields, returning_fields, raw)
|
645
634
|
if results:
|
646
635
|
for value, field in zip(results[0], returning_fields):
|
647
636
|
setattr(self, field.attname, value)
|
@@ -738,7 +727,7 @@ class Model(metaclass=ModelBase):
|
|
738
727
|
q = Q.create([(field.name, param), (f"id__{op}", self.id)], connector=Q.AND)
|
739
728
|
q = Q.create([q, (f"{field.name}__{op}", param)], connector=Q.OR)
|
740
729
|
qs = (
|
741
|
-
self.__class__.
|
730
|
+
self.__class__.query.filter(**kwargs)
|
742
731
|
.filter(q)
|
743
732
|
.order_by(f"{order}{field.name}", f"{order}id")
|
744
733
|
)
|
@@ -836,7 +825,7 @@ class Model(metaclass=ModelBase):
|
|
836
825
|
if len(unique_check) != len(lookup_kwargs):
|
837
826
|
continue
|
838
827
|
|
839
|
-
qs = model_class.
|
828
|
+
qs = model_class.query.filter(**lookup_kwargs)
|
840
829
|
|
841
830
|
# Exclude the current object from the query if we are editing an
|
842
831
|
# instance (as opposed to creating a new one)
|
@@ -995,9 +984,7 @@ class Model(metaclass=ModelBase):
|
|
995
984
|
|
996
985
|
@classmethod
|
997
986
|
def check(cls, **kwargs):
|
998
|
-
errors = [
|
999
|
-
*cls._check_managers(**kwargs),
|
1000
|
-
]
|
987
|
+
errors = []
|
1001
988
|
|
1002
989
|
database = kwargs.get("database", False)
|
1003
990
|
errors += [
|
@@ -1045,14 +1032,6 @@ class Model(metaclass=ModelBase):
|
|
1045
1032
|
)
|
1046
1033
|
return errors
|
1047
1034
|
|
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
1035
|
@classmethod
|
1057
1036
|
def _check_fields(cls, **kwargs):
|
1058
1037
|
"""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.
|
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.
|
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.
|
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.
|
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.
|
558
|
+
click.secho("Operations to perform:", fg="cyan")
|
559
559
|
|
560
560
|
if target_package_labels_only:
|
561
|
-
click.
|
561
|
+
click.secho(
|
562
562
|
" Apply all migrations: "
|
563
563
|
+ (", ".join(sorted({a for a, n in targets})) or "(none)"),
|
564
|
-
|
564
|
+
fg="yellow",
|
565
565
|
)
|
566
566
|
else:
|
567
|
-
click.
|
567
|
+
click.secho(
|
568
568
|
f" Target specific migration: {targets[0][1]}, from {targets[0][0]}",
|
569
|
-
|
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
|
590
|
+
), # Have to pass this in manually
|
597
591
|
)
|
598
592
|
print()
|
599
593
|
|
600
594
|
if verbosity >= 1:
|
601
|
-
click.
|
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.
|
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.
|
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.
|
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 []) + [
|