Skip to content

Commit

Permalink
Add QuerySetAny as a non-generic variant of QuerySet.
Browse files Browse the repository at this point in the history
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 typeddjango#704.

Signed-off-by: Zixuan James Li <p359101898@gmail.com>
  • Loading branch information
PIG208 committed Oct 27, 2022
1 parent 36002a2 commit dc16b5e
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 2 deletions.
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,6 +270,30 @@ 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 `django_stubs_ext.QuerySetAny`, a non-generic
variant of `QuerySet` dedicated for runtime type checking:

```python
from django_stubs_ext import QuerySetAny

def foo(obj: object) -> None:
isinstance(obj, QuerySetAny) # OK
```


## 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]: ...

_QuerySetAny = _QuerySet

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 QuerySetAny as QuerySetAny
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",
"QuerySetAny",
"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, _QuerySetAny, _Row
from django.utils.functional import _StrOrPromise as StrOrPromise
from django.utils.functional import _StrPromise as StrPromise

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

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

__all__ = ["StrOrPromise", "StrPromise", "ValuesQuerySet"]
__all__ = ["StrOrPromise", "StrPromise", "QuerySetAny", "ValuesQuerySet"]
45 changes: 45 additions & 0 deletions tests/typecheck/managers/querysets/test_querysetalias.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
- case: queryset_isinstance_check
main: |
from typing import Any
from django.db.models.query import QuerySet
from django_stubs_ext import QuerySetAny
def foo(q: QuerySet[Any]) -> None:
pass
def bar(q: QuerySetAny) -> None:
pass
def baz(obj: object) -> None:
if isinstance(obj, QuerySetAny):
reveal_type(obj) # N: Revealed type is "django.db.models.query._QuerySet[Any, 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)
- case: queryset_list
main: |
from typing import List
from django.db.models.query import QuerySet
from django_stubs_ext import QuerySetAny
from myapp.models import User, Book
def try_append(queryset_instance: QuerySetAny, queryset: QuerySet[User], queryset_book: QuerySet[Book]) -> None:
user_querysets: List[QuerySet[User]] = []
user_querysets.append(queryset_instance)
user_querysets.append(queryset)
user_querysets.append(queryset_book) # E: Argument 1 to "append" of "list" has incompatible type "_QuerySet[Book, Book]"; expected "_QuerySet[User, User]"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
pass
class Book(models.Model):
pass

0 comments on commit dc16b5e

Please sign in to comment.