diff --git a/README.md b/README.md index 8c07d28037..3f191cb766 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/django-stubs/db/models/query.pyi b/django-stubs/db/models/query.pyi index ec98170092..7301b14fcf 100644 --- a/django-stubs/db/models/query.pyi +++ b/django-stubs/db/models/query.pyi @@ -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: diff --git a/django_stubs_ext/django_stubs_ext/__init__.py b/django_stubs_ext/django_stubs_ext/__init__.py index 6fd35f3b26..ba252b8d35 100644 --- a/django_stubs_ext/django_stubs_ext/__init__.py +++ b/django_stubs_ext/django_stubs_ext/__init__.py @@ -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 @@ -7,6 +8,7 @@ __all__ = [ "monkeypatch", + "QuerySetAny", "ValuesQuerySet", "WithAnnotations", "Annotations", diff --git a/django_stubs_ext/django_stubs_ext/aliases.py b/django_stubs_ext/django_stubs_ext/aliases.py index 90ab07db90..840df17cd2 100644 --- a/django_stubs_ext/django_stubs_ext/aliases.py +++ b/django_stubs_ext/django_stubs_ext/aliases.py @@ -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"] diff --git a/tests/typecheck/managers/querysets/test_querysetalias.yml b/tests/typecheck/managers/querysets/test_querysetalias.yml new file mode 100644 index 0000000000..beaa12f0ba --- /dev/null +++ b/tests/typecheck/managers/querysets/test_querysetalias.yml @@ -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