From d5119af9cb55e1653299c726cbeffd234b4d779e Mon Sep 17 00:00:00 2001 From: Zixuan James Li Date: Fri, 21 Oct 2022 14:01:04 -0400 Subject: [PATCH] Add QuerySetAny as a non-generic variant of QuerySet. 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 #704. Signed-off-by: Zixuan James Li --- README.md | 26 +++++++++++ django-stubs/db/models/query.pyi | 2 + django_stubs_ext/django_stubs_ext/__init__.py | 2 + django_stubs_ext/django_stubs_ext/aliases.py | 6 ++- .../managers/querysets/test_querysetany.yml | 45 +++++++++++++++++++ 5 files changed, 79 insertions(+), 2 deletions(-) create mode 100644 tests/typecheck/managers/querysets/test_querysetany.yml diff --git a/README.md b/README.md index 8c07d2803..820127062 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: + 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 + +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. diff --git a/django-stubs/db/models/query.pyi b/django-stubs/db/models/query.pyi index ec9817009..7301b14fc 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 6fd35f3b2..ba252b8d3 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 90ab07db9..840df17cd 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_querysetany.yml b/tests/typecheck/managers/querysets/test_querysetany.yml new file mode 100644 index 000000000..beaa12f0b --- /dev/null +++ b/tests/typecheck/managers/querysets/test_querysetany.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