Skip to content

Commit e88f942

Browse files
authored
Add QuerySetAny as a non-generic variant of QuerySet. (#1199)
This also re-export `QuerySetAny` for external access to nongeneric QuerySet. The approach taken here is making `_QuerySetAny` an alias of `_QuerySet[_T, _T]` dedicated for isinstance checks, and leave `QuerySet` unchanged as the type alias of `_QuerySet[_T, _T]`. Fixes #704. Signed-off-by: Zixuan James Li <[email protected]> Signed-off-by: Zixuan James Li <[email protected]>
1 parent 1d78b8f commit e88f942

File tree

5 files changed

+79
-2
lines changed

5 files changed

+79
-2
lines changed

README.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,6 +271,32 @@ func(MyModel.objects.annotate(foo=Value("")).get(id=1)) # OK
271271
func(MyModel.objects.annotate(bar=Value("")).get(id=1)) # Error
272272
```
273273

274+
### How do I check if something is an instance of QuerySet in runtime?
275+
276+
A limitation of making `QuerySet` generic is that you can not use
277+
it for `isinstance` checks.
278+
279+
```python
280+
from django.db.models.query import QuerySet
281+
282+
def foo(obj: object) -> None:
283+
if isinstance(obj, QuerySet): # Error: Parameterized generics cannot be used with class or instance checks
284+
...
285+
```
286+
287+
To get around with this issue without making `QuerySet` non-generic,
288+
Django-stubs provides `django_stubs_ext.QuerySetAny`, a non-generic
289+
variant of `QuerySet` suitable for runtime type checking:
290+
291+
```python
292+
from django_stubs_ext import QuerySetAny
293+
294+
def foo(obj: object) -> None:
295+
if isinstance(obj, QuerySetAny): # OK
296+
...
297+
```
298+
299+
274300
## Related projects
275301

276302
- [`awesome-python-typing`](https://github.com/typeddjango/awesome-python-typing) - Awesome list of all typing-related things in Python.

django-stubs/db/models/query.pyi

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,8 @@ class RawQuerySet(Iterable[_T], Sized):
209209
def resolve_model_init_order(self) -> Tuple[List[str], List[int], List[Tuple[str, int]]]: ...
210210
def using(self, alias: Optional[str]) -> RawQuerySet[_T]: ...
211211

212+
_QuerySetAny = _QuerySet
213+
212214
QuerySet = _QuerySet[_T, _T]
213215

214216
class Prefetch:

django_stubs_ext/django_stubs_ext/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
from .aliases import QuerySetAny as QuerySetAny
12
from .aliases import StrOrPromise, StrPromise
23
from .aliases import ValuesQuerySet as ValuesQuerySet
34
from .annotations import Annotations as Annotations
@@ -7,6 +8,7 @@
78

89
__all__ = [
910
"monkeypatch",
11+
"QuerySetAny",
1012
"ValuesQuerySet",
1113
"WithAnnotations",
1214
"Annotations",
Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
11
import typing
22

33
if typing.TYPE_CHECKING:
4-
from django.db.models.query import _T, _QuerySet, _Row
4+
from django.db.models.query import _T, _QuerySet, _QuerySetAny, _Row
55
from django.utils.functional import _StrOrPromise as StrOrPromise
66
from django.utils.functional import _StrPromise as StrPromise
77

8+
QuerySetAny = _QuerySetAny
89
ValuesQuerySet = _QuerySet[_T, _Row]
910
else:
1011
from django.db.models.query import QuerySet
1112
from django.utils.functional import Promise as StrPromise
1213

14+
QuerySetAny = QuerySet
1315
ValuesQuerySet = QuerySet
1416
StrOrPromise = typing.Union[str, StrPromise]
1517

16-
__all__ = ["StrOrPromise", "StrPromise", "ValuesQuerySet"]
18+
__all__ = ["StrOrPromise", "StrPromise", "QuerySetAny", "ValuesQuerySet"]
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
- case: queryset_isinstance_check
2+
main: |
3+
from typing import Any
4+
from django.db.models.query import QuerySet
5+
from django_stubs_ext import QuerySetAny
6+
7+
def foo(q: QuerySet[Any]) -> None:
8+
pass
9+
10+
def bar(q: QuerySetAny) -> None:
11+
pass
12+
13+
def baz(obj: object) -> None:
14+
if isinstance(obj, QuerySetAny):
15+
reveal_type(obj) # N: Revealed type is "django.db.models.query._QuerySet[Any, Any]"
16+
foo(obj)
17+
bar(obj)
18+
19+
if isinstance(obj, QuerySet): # E: Parameterized generics cannot be used with class or instance checks
20+
reveal_type(obj) # N: Revealed type is "django.db.models.query._QuerySet[Any, Any]"
21+
foo(obj)
22+
bar(obj)
23+
- case: queryset_list
24+
main: |
25+
from typing import List
26+
from django.db.models.query import QuerySet
27+
from django_stubs_ext import QuerySetAny
28+
from myapp.models import User, Book
29+
30+
def try_append(queryset_instance: QuerySetAny, queryset: QuerySet[User], queryset_book: QuerySet[Book]) -> None:
31+
user_querysets: List[QuerySet[User]] = []
32+
user_querysets.append(queryset_instance)
33+
user_querysets.append(queryset)
34+
user_querysets.append(queryset_book) # E: Argument 1 to "append" of "list" has incompatible type "_QuerySet[Book, Book]"; expected "_QuerySet[User, User]"
35+
installed_apps:
36+
- myapp
37+
files:
38+
- path: myapp/__init__.py
39+
- path: myapp/models.py
40+
content: |
41+
from django.db import models
42+
class User(models.Model):
43+
pass
44+
class Book(models.Model):
45+
pass

0 commit comments

Comments
 (0)