plain.models 0.50.0__py3-none-any.whl → 0.51.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (49) hide show
  1. plain/models/CHANGELOG.md +14 -0
  2. plain/models/README.md +26 -42
  3. plain/models/__init__.py +2 -0
  4. plain/models/backends/base/creation.py +2 -2
  5. plain/models/backends/base/introspection.py +8 -4
  6. plain/models/backends/base/schema.py +89 -71
  7. plain/models/backends/base/validation.py +1 -1
  8. plain/models/backends/mysql/compiler.py +1 -1
  9. plain/models/backends/mysql/operations.py +1 -1
  10. plain/models/backends/mysql/schema.py +4 -4
  11. plain/models/backends/postgresql/operations.py +1 -1
  12. plain/models/backends/postgresql/schema.py +3 -3
  13. plain/models/backends/sqlite3/operations.py +1 -1
  14. plain/models/backends/sqlite3/schema.py +61 -50
  15. plain/models/base.py +116 -163
  16. plain/models/cli.py +4 -4
  17. plain/models/constraints.py +14 -9
  18. plain/models/deletion.py +15 -14
  19. plain/models/expressions.py +1 -1
  20. plain/models/fields/__init__.py +20 -16
  21. plain/models/fields/json.py +3 -3
  22. plain/models/fields/related.py +73 -71
  23. plain/models/fields/related_descriptors.py +2 -2
  24. plain/models/fields/related_lookups.py +1 -1
  25. plain/models/fields/related_managers.py +21 -32
  26. plain/models/fields/reverse_related.py +8 -8
  27. plain/models/forms.py +12 -12
  28. plain/models/indexes.py +5 -4
  29. plain/models/meta.py +505 -0
  30. plain/models/migrations/operations/base.py +1 -1
  31. plain/models/migrations/operations/fields.py +6 -6
  32. plain/models/migrations/operations/models.py +18 -16
  33. plain/models/migrations/recorder.py +9 -5
  34. plain/models/migrations/state.py +35 -46
  35. plain/models/migrations/utils.py +1 -1
  36. plain/models/options.py +182 -518
  37. plain/models/preflight.py +7 -5
  38. plain/models/query.py +119 -65
  39. plain/models/query_utils.py +18 -13
  40. plain/models/registry.py +6 -5
  41. plain/models/sql/compiler.py +51 -37
  42. plain/models/sql/query.py +77 -68
  43. plain/models/sql/subqueries.py +4 -4
  44. plain/models/utils.py +4 -1
  45. {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/METADATA +27 -43
  46. {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/RECORD +49 -48
  47. {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/WHEEL +0 -0
  48. {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/entry_points.txt +0 -0
  49. {plain_models-0.50.0.dist-info → plain_models-0.51.0.dist-info}/licenses/LICENSE +0 -0
plain/models/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # plain-models changelog
2
2
 
3
+ ## [0.51.0](https://github.com/dropseed/plain/releases/plain-models@0.51.0) (2025-10-07)
4
+
5
+ ### What's changed
6
+
7
+ - Model metadata has been split into two separate descriptors: `model_options` for user-defined configuration and `_model_meta` for internal metadata ([73ba469](https://github.com/dropseed/plain/commit/73ba469ba0), [17a378d](https://github.com/dropseed/plain/commit/17a378dcfb))
8
+ - The `_meta` attribute has been replaced with `model_options` for user-defined options like indexes, constraints, and database settings ([17a378d](https://github.com/dropseed/plain/commit/17a378dcfb))
9
+ - Custom QuerySets are now assigned directly to the `query` class attribute instead of using `Meta.queryset_class` ([2578301](https://github.com/dropseed/plain/commit/2578301819))
10
+ - Added comprehensive type improvements to model metadata and related fields for better IDE support ([3b477a0](https://github.com/dropseed/plain/commit/3b477a0d43))
11
+
12
+ ### Upgrade instructions
13
+
14
+ - Replace `Meta.queryset_class = CustomQuerySet` with `query = CustomQuerySet()` as a class attribute on your models
15
+ - Replace `class Meta:` with `model_options = models.Options(...)` in your models
16
+
3
17
  ## [0.50.0](https://github.com/dropseed/plain/releases/plain-models@0.50.0) (2025-10-06)
4
18
 
5
19
  ### What's changed
plain/models/README.md CHANGED
@@ -200,31 +200,32 @@ class User(models.Model):
200
200
  username = models.CharField(max_length=150)
201
201
  age = models.IntegerField()
202
202
 
203
- class Meta:
204
- indexes = [
203
+ model_options = models.Options(
204
+ indexes=[
205
205
  models.Index(fields=["email"]),
206
206
  models.Index(fields=["-created_at"], name="user_created_idx"),
207
- ]
208
- constraints = [
207
+ ],
208
+ constraints=[
209
209
  models.UniqueConstraint(fields=["email", "username"], name="unique_user"),
210
210
  models.CheckConstraint(check=models.Q(age__gte=0), name="age_positive"),
211
- ]
211
+ ],
212
+ )
212
213
  ```
213
214
 
214
215
  ## Custom QuerySets
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
217
+ With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods.
219
218
 
220
- Use `Meta.queryset_class` to set a custom QuerySet that will be used by `Model.query`:
219
+ Define a custom QuerySet and assign it to your model's `query` attribute:
221
220
 
222
221
  ```python
223
- class PublishedQuerySet(models.QuerySet):
224
- def published_only(self):
222
+ from typing import Self
223
+
224
+ class PublishedQuerySet(models.QuerySet["Article"]):
225
+ def published_only(self) -> Self:
225
226
  return self.filter(status="published")
226
227
 
227
- def draft_only(self):
228
+ def draft_only(self) -> Self:
228
229
  return self.filter(status="draft")
229
230
 
230
231
  @models.register_model
@@ -232,50 +233,33 @@ class Article(models.Model):
232
233
  title = models.CharField(max_length=200)
233
234
  status = models.CharField(max_length=20)
234
235
 
235
- class Meta:
236
- queryset_class = PublishedQuerySet
236
+ query = PublishedQuerySet()
237
237
 
238
- # Usage - all methods available on Article.objects
238
+ # Usage - all methods available on Article.query
239
239
  all_articles = Article.query.all()
240
240
  published_articles = Article.query.published_only()
241
241
  draft_articles = Article.query.draft_only()
242
242
  ```
243
243
 
244
- ### Using custom QuerySets without formal attachment
245
-
246
- You can also use custom QuerySets manually without setting them as the default:
244
+ Custom methods can be chained with built-in QuerySet methods:
247
245
 
248
246
  ```python
249
- class SpecialQuerySet(models.QuerySet):
250
- def special_filter(self):
251
- return self.filter(special=True)
252
-
253
- # Create and use the QuerySet manually
254
- special_qs = SpecialQuerySet(model=Article)
255
- special_articles = special_qs.special_filter()
247
+ # Chaining works naturally
248
+ recent_published = Article.query.published_only().order_by("-created_at")[:10]
256
249
  ```
257
250
 
258
- ### Using classmethods for convenience
251
+ ### Programmatic QuerySet usage
259
252
 
260
- For even cleaner API, add classmethods to your model:
253
+ For internal code that needs to create QuerySet instances programmatically, use `from_model()`:
261
254
 
262
255
  ```python
263
- @models.register_model
264
- class Article(models.Model):
265
- title = models.CharField(max_length=200)
266
- status = models.CharField(max_length=20)
267
-
268
- @classmethod
269
- def published(cls):
270
- return PublishedQuerySet(model=cls).published_only()
271
-
272
- @classmethod
273
- def drafts(cls):
274
- return PublishedQuerySet(model=cls).draft_only()
256
+ class SpecialQuerySet(models.QuerySet["Article"]):
257
+ def special_filter(self) -> Self:
258
+ return self.filter(special=True)
275
259
 
276
- # Usage
277
- published_articles = Article.published()
278
- draft_articles = Article.drafts()
260
+ # Create and use the QuerySet programmatically
261
+ special_qs = SpecialQuerySet.from_model(Article)
262
+ special_articles = special_qs.special_filter()
279
263
  ```
280
264
 
281
265
  ## Forms
plain/models/__init__.py CHANGED
@@ -63,6 +63,7 @@ from .registry import models_registry, register_model
63
63
 
64
64
  # Imports that would create circular imports if sorted
65
65
  from .base import DEFERRED, Model # isort:skip
66
+ from .options import Options # isort:skip
66
67
  from .fields.related import ( # isort:skip
67
68
  ForeignKey,
68
69
  ManyToManyField,
@@ -104,6 +105,7 @@ __all__ += [
104
105
  "JSONField",
105
106
  "Lookup",
106
107
  "Transform",
108
+ "Options",
107
109
  "Prefetch",
108
110
  "Q",
109
111
  "QuerySet",
@@ -98,7 +98,7 @@ class BaseDatabaseCreation:
98
98
  # and package_config.package_label in loader.migrated_packages
99
99
  # ):
100
100
  # for model in package_config.get_models():
101
- # if model._meta.can_migrate(
101
+ # if model.model_options.can_migrate(
102
102
  # self.connection
103
103
  # ) and router.allow_migrate_model(self.connection.alias, model):
104
104
  # queryset = model._base_manager.using(
@@ -127,7 +127,7 @@ class BaseDatabaseCreation:
127
127
  # "json", data, using=self.connection.alias
128
128
  # ):
129
129
  # obj.save()
130
- # table_names.add(obj.object.__class__._meta.db_table)
130
+ # table_names.add(obj.object.model_options.db_table)
131
131
  # # Manually check for any invalid keys that might have been added,
132
132
  # # because constraint checks were disabled.
133
133
  # self.connection.check_constraints(table_names=table_names)
@@ -94,7 +94,7 @@ class BaseDatabaseIntrospection:
94
94
  for model in models_registry.get_models(
95
95
  package_label=package_config.package_label
96
96
  )
97
- if model._meta.can_migrate(self.connection)
97
+ if model.model_options.can_migrate(self.connection)
98
98
  )
99
99
 
100
100
  def plain_table_names(
@@ -108,8 +108,10 @@ class BaseDatabaseIntrospection:
108
108
  """
109
109
  tables = set()
110
110
  for model in self.get_migratable_models():
111
- tables.add(model._meta.db_table)
112
- tables.update(f.m2m_db_table() for f in model._meta.local_many_to_many)
111
+ tables.add(model.model_options.db_table)
112
+ tables.update(
113
+ f.m2m_db_table() for f in model._model_meta.local_many_to_many
114
+ )
113
115
  tables = list(tables)
114
116
  if only_existing:
115
117
  existing_tables = set(self.table_names(include_views=include_views))
@@ -128,7 +130,9 @@ class BaseDatabaseIntrospection:
128
130
  for model in self.get_migratable_models():
129
131
  sequence_list.extend(
130
132
  self.get_sequences(
131
- cursor, model._meta.db_table, model._meta.local_fields
133
+ cursor,
134
+ model.model_options.db_table,
135
+ model._model_meta.local_fields,
132
136
  )
133
137
  )
134
138
  return sequence_list