Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Introduce QuerySetAny type for QuerySet isinstance checks #1199

Merged
merged 1 commit into from
Nov 3, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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:
if 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` suitable for runtime type checking:

```python
from django_stubs_ext import QuerySetAny
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Question for other PRs: do we have other types to take care of?


def foo(obj: object) -> None:
if 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_querysetany.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