diff --git a/README.md b/README.md index 8c07d28037..1aea7e367c 100644 --- a/README.md +++ b/README.md @@ -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. diff --git a/django-stubs/db/models/query.pyi b/django-stubs/db/models/query.pyi index ec98170092..69a0ea6e21 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]: ... +class _QuerySetAlias(_QuerySet[_T, _T]): ... + 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..1403ab75fa 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 QuerySetAlias as QuerySetAlias from .aliases import StrOrPromise, StrPromise from .aliases import ValuesQuerySet as ValuesQuerySet from .annotations import Annotations as Annotations @@ -7,6 +8,7 @@ __all__ = [ "monkeypatch", + "QuerySetAlias", "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..9d2316df54 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, _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"] diff --git a/tests/typecheck/managers/querysets/test_querysetalias.yml b/tests/typecheck/managers/querysets/test_querysetalias.yml new file mode 100644 index 0000000000..622ee65d71 --- /dev/null +++ b/tests/typecheck/managers/querysets/test_querysetalias.yml @@ -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]"