Skip to content

Commit 2578301

Browse files
committed
QuerySet class as a descriptor
1 parent 6a44350 commit 2578301

File tree

15 files changed

+168
-166
lines changed

15 files changed

+168
-166
lines changed

plain-cache/plain/cache/chores.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ def clear_expired() -> str:
88
"""
99
Delete cache items that have expired.
1010
"""
11-
result = CachedItem.query.expired().delete() # type: ignore[attr-defined]
11+
result = CachedItem.query.expired().delete()
1212
return f"{result[0]} expired cache items deleted"

plain-cache/plain/cache/cli.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ def cli() -> None:
1414
@cli.command()
1515
def clear_expired() -> None:
1616
click.echo("Clearing expired cache items...")
17-
result = CachedItem.query.expired().delete() # type: ignore[attr-defined]
17+
result = CachedItem.query.expired().delete()
1818
click.echo(f"Deleted {result[0]} expired cache items.")
1919

2020

@@ -33,9 +33,9 @@ def clear_all(force: bool) -> None:
3333
@cli.command()
3434
def stats() -> None:
3535
total = CachedItem.query.count()
36-
expired = CachedItem.query.expired().count() # type: ignore[attr-defined]
37-
unexpired = CachedItem.query.unexpired().count() # type: ignore[attr-defined]
38-
forever = CachedItem.query.forever().count() # type: ignore[attr-defined]
36+
expired = CachedItem.query.expired().count()
37+
unexpired = CachedItem.query.unexpired().count()
38+
forever = CachedItem.query.forever().count()
3939

4040
click.echo(f"Total: {click.style(total, bold=True)}")
4141
click.echo(f"Expired: {click.style(expired, bold=True)}")

plain-cache/plain/cache/models.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
11
from __future__ import annotations
22

3+
from typing import Self
4+
35
from plain import models
46
from plain.utils import timezone
57

68

7-
class CachedItemQuerySet(models.QuerySet):
8-
def expired(self) -> CachedItemQuerySet:
9+
class CachedItemQuerySet(models.QuerySet["CachedItem"]):
10+
def expired(self) -> Self:
911
return self.filter(expires_at__lt=timezone.now())
1012

11-
def unexpired(self) -> CachedItemQuerySet:
13+
def unexpired(self) -> Self:
1214
return self.filter(expires_at__gte=timezone.now())
1315

14-
def forever(self) -> CachedItemQuerySet:
16+
def forever(self) -> Self:
1517
return self.filter(expires_at=None)
1618

1719

@@ -23,8 +25,9 @@ class CachedItem(models.Model):
2325
created_at = models.DateTimeField(auto_now_add=True)
2426
updated_at = models.DateTimeField(auto_now=True)
2527

28+
query = CachedItemQuerySet()
29+
2630
class Meta:
27-
queryset_class = CachedItemQuerySet
2831
indexes = [
2932
models.Index(fields=["expires_at"]),
3033
]

plain-models/plain/models/README.md

Lines changed: 20 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -213,69 +213,52 @@ class User(models.Model):
213213

214214
## Custom QuerySets
215215

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:
216+
With the Manager functionality now merged into QuerySet, you can customize [`QuerySet`](./query.py#QuerySet) classes to provide specialized query methods.
217217

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`:
218+
Define a custom QuerySet and assign it to your model's `query` attribute:
221219

222220
```python
223-
class PublishedQuerySet(models.QuerySet):
224-
def published_only(self):
221+
from typing import Self
222+
223+
class PublishedQuerySet(models.QuerySet["Article"]):
224+
def published_only(self) -> Self:
225225
return self.filter(status="published")
226226

227-
def draft_only(self):
227+
def draft_only(self) -> Self:
228228
return self.filter(status="draft")
229229

230230
@models.register_model
231231
class Article(models.Model):
232232
title = models.CharField(max_length=200)
233233
status = models.CharField(max_length=20)
234234

235-
class Meta:
236-
queryset_class = PublishedQuerySet
235+
query = PublishedQuerySet()
237236

238-
# Usage - all methods available on Article.objects
237+
# Usage - all methods available on Article.query
239238
all_articles = Article.query.all()
240239
published_articles = Article.query.published_only()
241240
draft_articles = Article.query.draft_only()
242241
```
243242

244-
### Using custom QuerySets without formal attachment
245-
246-
You can also use custom QuerySets manually without setting them as the default:
243+
Custom methods can be chained with built-in QuerySet methods:
247244

248245
```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()
246+
# Chaining works naturally
247+
recent_published = Article.query.published_only().order_by("-created_at")[:10]
256248
```
257249

258-
### Using classmethods for convenience
250+
### Programmatic QuerySet usage
259251

260-
For even cleaner API, add classmethods to your model:
252+
For internal code that needs to create QuerySet instances programmatically, use `from_model()`:
261253

262254
```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()
255+
class SpecialQuerySet(models.QuerySet["Article"]):
256+
def special_filter(self) -> Self:
257+
return self.filter(special=True)
275258

276-
# Usage
277-
published_articles = Article.published()
278-
draft_articles = Article.drafts()
259+
# Create and use the QuerySet programmatically
260+
special_qs = SpecialQuerySet.from_model(Article)
261+
special_articles = special_qs.special_filter()
279262
```
280263

281264
## Forms

plain-models/plain/models/base.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -122,11 +122,6 @@ def _setup_meta(cls) -> None:
122122

123123
Options(meta, package_label).contribute_to_class(cls, "_meta")
124124

125-
@property
126-
def query(cls) -> QuerySet:
127-
"""Create a new QuerySet for this model."""
128-
return cls._meta.queryset
129-
130125

131126
class ModelStateFieldsCacheDescriptor:
132127
def __get__(
@@ -159,6 +154,9 @@ class Model(metaclass=ModelBase):
159154
# Every model gets an automatic id field
160155
id = PrimaryKeyField()
161156

157+
# QuerySet descriptor for model queries
158+
query = QuerySet()
159+
162160
def __init__(self, *args: Any, **kwargs: Any):
163161
# Alias some things as locals to avoid repeat global lookups
164162
cls = self.__class__

plain-models/plain/models/fields/related_managers.py

Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ def __init__(self, instance: Any, rel: Any):
7070
self.instance = instance
7171
self.field = rel.field
7272
self.core_filters = {self.field.name: instance}
73-
# Store the base queryset class for this model
74-
self.base_queryset_class = rel.related_model._meta.queryset.__class__
7573
self.allow_null = rel.field.allow_null
7674

7775
def _check_fk_val(self) -> None:
@@ -137,15 +135,14 @@ def get_queryset(self) -> QuerySet:
137135
self.field.remote_field.get_cache_name()
138136
]
139137
except (AttributeError, KeyError):
140-
# Use the base queryset class for this model
141-
queryset = self.base_queryset_class(model=self.model)
138+
queryset = self.model.query
142139
return self._apply_rel_filters(queryset)
143140

144141
def get_prefetch_queryset(
145142
self, instances: Any, queryset: QuerySet | None = None
146143
) -> tuple[QuerySet, Any, Any, bool, str, bool]:
147144
if queryset is None:
148-
queryset = self.base_queryset_class(model=self.model)
145+
queryset = self.model.query
149146

150147
rel_obj_attr = self.field.get_local_related_value
151148
instance_attr = self.field.get_foreign_related_value
@@ -196,17 +193,17 @@ def check_and_update_obj(obj: Any) -> None:
196193
def create(self, **kwargs: Any) -> Any:
197194
self._check_fk_val()
198195
kwargs[self.field.name] = self.instance
199-
return self.base_queryset_class(model=self.model).create(**kwargs)
196+
return self.model.query.create(**kwargs)
200197

201198
def get_or_create(self, **kwargs: Any) -> tuple[Any, bool]:
202199
self._check_fk_val()
203200
kwargs[self.field.name] = self.instance
204-
return self.base_queryset_class(model=self.model).get_or_create(**kwargs)
201+
return self.model.query.get_or_create(**kwargs)
205202

206203
def update_or_create(self, **kwargs: Any) -> tuple[Any, bool]:
207204
self._check_fk_val()
208205
kwargs[self.field.name] = self.instance
209-
return self.base_queryset_class(model=self.model).update_or_create(**kwargs)
206+
return self.model.query.update_or_create(**kwargs)
210207

211208
def remove(self, *objs: Any, bulk: bool = True) -> None:
212209
# remove() is only provided if the ForeignKey can have a value of null
@@ -297,8 +294,6 @@ class BaseManyToManyManager(BaseRelatedManager):
297294
def __init__(self, instance: Any, rel: Any):
298295
self.instance = instance
299296
self.through = rel.through
300-
# Subclasses must set model before calling super().__init__
301-
self.base_queryset_class = self.model._meta.queryset.__class__
302297

303298
self.source_field = self.through._meta.get_field(self.source_field_name)
304299
self.target_field = self.through._meta.get_field(self.target_field_name)
@@ -338,14 +333,14 @@ def get_queryset(self) -> QuerySet:
338333
try:
339334
return self.instance._prefetched_objects_cache[self.prefetch_cache_name]
340335
except (AttributeError, KeyError):
341-
queryset = self.base_queryset_class(model=self.model)
336+
queryset = self.model.query
342337
return self._apply_rel_filters(queryset)
343338

344339
def get_prefetch_queryset(
345340
self, instances: Any, queryset: QuerySet | None = None
346341
) -> tuple[QuerySet, Any, Any, bool, str, bool]:
347342
if queryset is None:
348-
queryset = self.base_queryset_class(model=self.model)
343+
queryset = self.model.query
349344

350345
queryset = _filter_prefetch_queryset(
351346
queryset._next_is_sticky(), self.query_field_name, instances
@@ -380,9 +375,7 @@ def get_prefetch_queryset(
380375
def clear(self) -> None:
381376
with transaction.atomic(savepoint=False):
382377
self._remove_prefetched_objects()
383-
filters = self._build_remove_filters(
384-
self.base_queryset_class(model=self.model)
385-
)
378+
filters = self._build_remove_filters(self.model.query)
386379
self.through.query.filter(filters).delete()
387380

388381
def set(
@@ -425,16 +418,14 @@ def set(
425418
def create(
426419
self, *, through_defaults: dict[str, Any] | None = None, **kwargs: Any
427420
) -> Any:
428-
new_obj = self.base_queryset_class(model=self.model).create(**kwargs)
421+
new_obj = self.model.query.create(**kwargs)
429422
self.add(new_obj, through_defaults=through_defaults)
430423
return new_obj
431424

432425
def get_or_create(
433426
self, *, through_defaults: dict[str, Any] | None = None, **kwargs: Any
434427
) -> tuple[Any, bool]:
435-
obj, created = self.base_queryset_class(model=self.model).get_or_create(
436-
**kwargs
437-
)
428+
obj, created = self.model.query.get_or_create(**kwargs)
438429
# We only need to add() if created because if we got an object back
439430
# from get() then the relationship already exists.
440431
if created:
@@ -444,9 +435,7 @@ def get_or_create(
444435
def update_or_create(
445436
self, *, through_defaults: dict[str, Any] | None = None, **kwargs: Any
446437
) -> tuple[Any, bool]:
447-
obj, created = self.base_queryset_class(model=self.model).update_or_create(
448-
**kwargs
449-
)
438+
obj, created = self.model.query.update_or_create(**kwargs)
450439
# We only need to add() if created because if we got an object back
451440
# from get() then the relationship already exists.
452441
if created:
@@ -534,7 +523,7 @@ def _remove_items(
534523
old_ids.add(obj)
535524

536525
with transaction.atomic(savepoint=False):
537-
target_model_qs = self.base_queryset_class(model=self.model)
526+
target_model_qs = self.model.query
538527
if target_model_qs._has_filters():
539528
old_vals = target_model_qs.filter(
540529
**{f"{self.target_field.target_field.attname}__in": old_ids}

plain-models/plain/models/options.py

Lines changed: 1 addition & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@
2929
DEFAULT_NAMES = (
3030
"db_table",
3131
"db_table_comment",
32-
"queryset_class",
3332
"ordering",
3433
"package_label",
3534
"models_registry",
@@ -53,7 +52,6 @@ class Options:
5352
"_non_pk_concrete_field_names",
5453
"_forward_fields_map",
5554
"base_queryset",
56-
"queryset",
5755
}
5856
REVERSE_PROPERTIES = {"related_objects", "fields_map", "_relation_tree"}
5957

@@ -63,7 +61,6 @@ def __init__(self, meta: Any, package_label: str | None = None):
6361
self._get_fields_cache: dict[tuple[bool, bool, bool, bool], Any] = {}
6462
self.local_fields: list[Field] = []
6563
self.local_many_to_many: list[Field] = []
66-
self.queryset_class: type[QuerySet] | None = None
6764
self.model_name: str | None = None
6865
self.db_table: str = ""
6966
self.db_table_comment: str = ""
@@ -220,13 +217,7 @@ def base_queryset(self) -> QuerySet:
220217
objects), the base queryset must never filter out rows to prevent
221218
incomplete results in related queries.
222219
"""
223-
return QuerySet(model=self.model)
224-
225-
@property
226-
def queryset(self) -> QuerySet:
227-
if self.queryset_class:
228-
return self.queryset_class(model=self.model)
229-
return QuerySet(model=self.model)
220+
return QuerySet.from_model(self.model)
230221

231222
@cached_property
232223
def fields(self) -> ImmutableList:

0 commit comments

Comments
 (0)