Skip to content

Commit

Permalink
Add QuerySetAlias as a non-generic variant of QuerySet.
Browse files Browse the repository at this point in the history
This also re-export `QuerySetAlias` for external access to
nongeneric QuerySet.

The approach taken here is making `_QuerySetAlias` a subclass
of `_QuerySet[_T, _T]` dedicated for isinstance checks, and
leave `QuerySet` unchanged as the type alias of `_QuerySet[_T, _T]`.

A slight disadvantage of this approach is that `QuerySet` is not
a subclass of `QuerySetAlias`, and thus using `QuerySetAlias` in
type annotation is generally incorrect. But the fix should suffice
for the `isinstance` check use case.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
  • Loading branch information
PIG208 committed Oct 21, 2022
1 parent 36002a2 commit 5c0ef2c
Show file tree
Hide file tree
Showing 5 changed files with 56 additions and 2 deletions.
26 changes: 26 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,32 @@ func(MyModel.objects.annotate(foo=Value("")).get(id=1)) # OK
func(MyModel.objects.annotate(bar=Value("")).get(id=1)) # Error
```
### How do I check if something is an instance of QuerySet in runtime?
A limitation of making `QuerySet` generic is that you can not use
it for `isinstance` checks.
```python
from django.db.models.query import QuerySet
def foo(obj: object) -> None:
isinstance(obj, QuerySet) # Error: Parameterized generics cannot be used with class or instance checks
```
To get around with this issue without making `QuerySet` non-generic,
Django-stubs provides a `django_stubs_ext.QuerySetAlias`, a non-generic
variant of `QuerySet` dedicated for runtime type checking:
```python
from django_stubs_ext import QuerySetAlias
def foo(obj: object) -> None:
isinstance(obj, QuerySetAlias) # OK
```
Note that `QuerySetAlias` is only used for runtime type checking,
it should not be used in type annotations in place of `QuerySet`.
## Related projects
- [`awesome-python-typing`](https://github.com/typeddjango/awesome-python-typing) - Awesome list of all typing-related things in Python.
Expand Down
2 changes: 2 additions & 0 deletions django-stubs/db/models/query.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,8 @@ class RawQuerySet(Iterable[_T], Sized):
def resolve_model_init_order(self) -> Tuple[List[str], List[int], List[Tuple[str, int]]]: ...
def using(self, alias: Optional[str]) -> RawQuerySet[_T]: ...

class _QuerySetAlias(_QuerySet[_T, _T]): ...

QuerySet = _QuerySet[_T, _T]

class Prefetch:
Expand Down
2 changes: 2 additions & 0 deletions django_stubs_ext/django_stubs_ext/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from .aliases import QuerySetAlias as QuerySetAlias
from .aliases import StrOrPromise, StrPromise
from .aliases import ValuesQuerySet as ValuesQuerySet
from .annotations import Annotations as Annotations
Expand All @@ -7,6 +8,7 @@

__all__ = [
"monkeypatch",
"QuerySetAlias",
"ValuesQuerySet",
"WithAnnotations",
"Annotations",
Expand Down
6 changes: 4 additions & 2 deletions django_stubs_ext/django_stubs_ext/aliases.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
import typing

if typing.TYPE_CHECKING:
from django.db.models.query import _T, _QuerySet, _Row
from django.db.models.query import _T, _QuerySet, _QuerySetAlias, _Row
from django.utils.functional import _StrOrPromise as StrOrPromise
from django.utils.functional import _StrPromise as StrPromise

QuerySetAlias = _QuerySetAlias
ValuesQuerySet = _QuerySet[_T, _Row]
else:
from django.db.models.query import QuerySet
from django.utils.functional import Promise as StrPromise

QuerySetAlias = QuerySet
ValuesQuerySet = QuerySet
StrOrPromise = typing.Union[str, StrPromise]

__all__ = ["StrOrPromise", "StrPromise", "ValuesQuerySet"]
__all__ = ["StrOrPromise", "StrPromise", "QuerySetAlias", "ValuesQuerySet"]
22 changes: 22 additions & 0 deletions tests/typecheck/managers/querysets/test_querysetalias.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
- case: queryset_isinstance_check
main: |
from typing import Any
from django.db.models.query import QuerySet
from django_stubs_ext import QuerySetAlias
def foo(q: QuerySet[Any]) -> None:
pass
def bar(q: QuerySetAlias) -> None:
pass
def baz(obj: object) -> None:
if isinstance(obj, QuerySetAlias):
reveal_type(obj) # N: Revealed type is "django.db.models.query._QuerySetAlias[Any]"
foo(obj)
bar(obj)
if isinstance(obj, QuerySet): # E: Parameterized generics cannot be used with class or instance checks
reveal_type(obj) # N: Revealed type is "django.db.models.query._QuerySet[Any, Any]"
foo(obj)
bar(obj) # E: Argument 1 to "bar" has incompatible type "_QuerySet[Any, Any]"; expected "_QuerySetAlias[Any]"

0 comments on commit 5c0ef2c

Please sign in to comment.