QuerySet.annotate improvements (#398)

* QuerySet.annotate returns self-type. Attribute access falls back to Any.

- QuerySets that have an annotated model do not report errors during .filter() when called with invalid fields.
- QuerySets that have an annotated model return ordinary dict rather than TypedDict for .values()
- QuerySets that have an annotated model return Any rather than typed Tuple for .values_list()

* Fix .annotate so it reuses existing annotated types. Fixes error in typechecking Django testsuite.

* Fix self-typecheck error

* Fix flake8

* Fix case of .values/.values_list before .annotate.

* Extra ignores for Django 2.2 tests (false positives due to tests assuming QuerySet.first() won't return None)

Fix mypy self-check.

* More tests + more precise typing in case annotate called before values_list.

Cleanup tests.

* Test and fix annotate in combination with values/values_list with no params.

* Remove line that does nothing :)

* Formatting fixes

* Address code review

* Fix quoting in tests after mypy changed things

* Use Final

* Use typing_extensions.Final

* Fixes after ValuesQuerySet -> _ValuesQuerySet refactor. Still not passing tests yet.

* Fix inheritance of _ValuesQuerySet and remove unneeded type ignores.

This allows the test
"annotate_values_or_values_list_before_or_after_annotate_broadens_type"
to pass.

* Make it possible to annotate user code with "annotated models", using PEP 583 Annotated type.

* Add docs

* Make QuerySet[_T] an external alias to _QuerySet[_T, _T].

This currently has the drawback that error messages display the internal type _QuerySet, with both type arguments.

See also discussion on #661 and #608.

Fixes #635: QuerySet methods on Managers (like .all()) now return QuerySets rather than Managers.

Address code review by @sobolevn.

* Support passing TypedDicts to WithAnnotations

* Add an example of an error to README regarding WithAnnotations + TypedDict.

* Fix runtime behavior of ValuesQuerySet alias (you can't extend Any, for example).

Fix some edge case with from_queryset after QuerySet changed to be an
alias to _QuerySet. Can't make a minimal test case as this only occurred
on a large internal codebase.

* Fix issue when using from_queryset in some cases when having an argument with a type annotation on the QuerySet.

The mypy docstring on anal_type says not to call defer() after it.
This commit is contained in:
Seth Yastrov
2021-07-23 15:15:15 +02:00
committed by GitHub
parent c69e720dd8
commit cfd69c0acc
25 changed files with 860 additions and 123 deletions

View File

@@ -179,6 +179,48 @@ def use_my_model():
return foo.xyz # Gives an error
```
### How do I annotate cases where I called QuerySet.annotate?
Django-stubs provides a special type, `django_stubs_ext.WithAnnotations[Model]`, which indicates that the `Model` has
been annotated, meaning it allows getting/setting extra attributes on the model instance.
Optionally, you can provide a `TypedDict` of these attributes,
e.g. `WithAnnotations[MyModel, MyTypedDict]`, to specify which annotated attributes are present.
Currently, the mypy plugin can recognize that specific names were passed to `QuerySet.annotate` and
include them in the type, but does not record the types of these attributes.
The knowledge of the specific annotated fields is not yet used in creating more specific types for `QuerySet`'s
`values`, `values_list`, or `filter` methods, however knowledge that the model was annotated _is_ used to create a
broader type result type for `values`/`values_list`, and to allow `filter`ing on any field.
```python
from typing import TypedDict
from django_stubs_ext import WithAnnotations
from django.db import models
from django.db.models.expressions import Value
class MyModel(models.Model):
username = models.CharField(max_length=100)
def func(m: WithAnnotations[MyModel]) -> str:
return m.asdf # OK, since the model is annotated as allowing any attribute
func(MyModel.objects.annotate(foo=Value("")).get(id=1)) # OK
func(MyModel.objects.get(id=1)) # Error, since this model will not allow access to any attribute
class MyTypedDict(TypedDict):
foo: str
def func2(m: WithAnnotations[MyModel, MyTypedDict]) -> str:
print(m.bar) # Error, since field "bar" is not in MyModel or MyTypedDict.
return m.foo # OK, since we said field "foo" was allowed
func(MyModel.objects.annotate(foo=Value("")).get(id=1)) # OK
func(MyModel.objects.annotate(bar=Value("")).get(id=1)) # Error
```
## Related projects

View File

@@ -1,4 +1,4 @@
from typing import Any, NamedTuple
from typing import Any, Protocol
from .utils.version import get_version as get_version
@@ -7,7 +7,7 @@ __version__: str
def setup(set_prefix: bool = ...) -> None: ...
# Used by mypy_django_plugin when returning a QuerySet row that is a NamedTuple where the field names are unknown
class _NamedTupleAnyAttr(NamedTuple):
# Used internally by mypy_django_plugin.
class _AnyAttrAllowed(Protocol):
def __getattr__(self, item: str) -> Any: ...
def __setattr__(self, item: str, value: Any) -> None: ...

View File

@@ -1,12 +1,30 @@
from typing import Any, Dict, Iterable, List, Optional, Tuple, Type, TypeVar, Union
import datetime
from typing import (
Any,
Dict,
Generic,
Iterable,
Iterator,
List,
MutableMapping,
Optional,
Sequence,
Tuple,
Type,
TypeVar,
Union,
)
from django.db.models import Combinable
from django.db.models.base import Model
from django.db.models.query import QuerySet
from django.db.models.query import QuerySet, RawQuerySet
from django_stubs_ext import ValuesQuerySet
_T = TypeVar("_T", bound=Model, covariant=True)
_M = TypeVar("_M", bound="BaseManager")
class BaseManager(QuerySet[_T]):
class BaseManager(Generic[_T]):
creation_counter: int = ...
auto_created: bool = ...
use_in_migrations: bool = ...
@@ -24,6 +42,80 @@ class BaseManager(QuerySet[_T]):
def contribute_to_class(self, model: Type[Model], name: str) -> None: ...
def db_manager(self: _M, using: Optional[str] = ..., hints: Optional[Dict[str, Model]] = ...) -> _M: ...
def get_queryset(self) -> QuerySet[_T]: ...
# NOTE: The following methods are in common with QuerySet, but note that the use of QuerySet as a return type
# rather than a self-type (_QS), since Manager's QuerySet-like methods return QuerySets and not Managers.
def iterator(self, chunk_size: int = ...) -> Iterator[_T]: ...
def aggregate(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: ...
def get(self, *args: Any, **kwargs: Any) -> _T: ...
def create(self, *args: Any, **kwargs: Any) -> _T: ...
def bulk_create(
self, objs: Iterable[_T], batch_size: Optional[int] = ..., ignore_conflicts: bool = ...
) -> List[_T]: ...
def bulk_update(self, objs: Iterable[_T], fields: Sequence[str], batch_size: Optional[int] = ...) -> None: ...
def get_or_create(self, defaults: Optional[MutableMapping[str, Any]] = ..., **kwargs: Any) -> Tuple[_T, bool]: ...
def update_or_create(
self, defaults: Optional[MutableMapping[str, Any]] = ..., **kwargs: Any
) -> Tuple[_T, bool]: ...
def earliest(self, *fields: Any, field_name: Optional[Any] = ...) -> _T: ...
def latest(self, *fields: Any, field_name: Optional[Any] = ...) -> _T: ...
def first(self) -> Optional[_T]: ...
def last(self) -> Optional[_T]: ...
def in_bulk(self, id_list: Iterable[Any] = ..., *, field_name: str = ...) -> Dict[Any, _T]: ...
def delete(self) -> Tuple[int, Dict[str, int]]: ...
def update(self, **kwargs: Any) -> int: ...
def exists(self) -> bool: ...
def explain(self, *, format: Optional[Any] = ..., **options: Any) -> str: ...
def raw(
self,
raw_query: str,
params: Any = ...,
translations: Optional[Dict[str, str]] = ...,
using: Optional[str] = ...,
) -> RawQuerySet: ...
# The type of values may be overridden to be more specific in the mypy plugin, depending on the fields param
def values(self, *fields: Union[str, Combinable], **expressions: Any) -> ValuesQuerySet[_T, Dict[str, Any]]: ...
# The type of values_list may be overridden to be more specific in the mypy plugin, depending on the fields param
def values_list(
self, *fields: Union[str, Combinable], flat: bool = ..., named: bool = ...
) -> ValuesQuerySet[_T, Any]: ...
def dates(self, field_name: str, kind: str, order: str = ...) -> ValuesQuerySet[_T, datetime.date]: ...
def datetimes(
self, field_name: str, kind: str, order: str = ..., tzinfo: Optional[datetime.tzinfo] = ...
) -> ValuesQuerySet[_T, datetime.datetime]: ...
def none(self) -> QuerySet[_T]: ...
def all(self) -> QuerySet[_T]: ...
def filter(self, *args: Any, **kwargs: Any) -> QuerySet[_T]: ...
def exclude(self, *args: Any, **kwargs: Any) -> QuerySet[_T]: ...
def complex_filter(self, filter_obj: Any) -> QuerySet[_T]: ...
def count(self) -> int: ...
def union(self, *other_qs: Any, all: bool = ...) -> QuerySet[_T]: ...
def intersection(self, *other_qs: Any) -> QuerySet[_T]: ...
def difference(self, *other_qs: Any) -> QuerySet[_T]: ...
def select_for_update(
self, nowait: bool = ..., skip_locked: bool = ..., of: Sequence[str] = ..., no_key: bool = ...
) -> QuerySet[_T]: ...
def select_related(self, *fields: Any) -> QuerySet[_T]: ...
def prefetch_related(self, *lookups: Any) -> QuerySet[_T]: ...
def annotate(self, *args: Any, **kwargs: Any) -> QuerySet[_T]: ...
def alias(self, *args: Any, **kwargs: Any) -> QuerySet[_T]: ...
def order_by(self, *field_names: Any) -> QuerySet[_T]: ...
def distinct(self, *field_names: Any) -> QuerySet[_T]: ...
# extra() return type won't be supported any time soon
def extra(
self,
select: Optional[Dict[str, Any]] = ...,
where: Optional[List[str]] = ...,
params: Optional[List[Any]] = ...,
tables: Optional[List[str]] = ...,
order_by: Optional[Sequence[str]] = ...,
select_params: Optional[Sequence[Any]] = ...,
) -> QuerySet[Any]: ...
def reverse(self) -> QuerySet[_T]: ...
def defer(self, *fields: Any) -> QuerySet[_T]: ...
def only(self, *fields: Any) -> QuerySet[_T]: ...
def using(self, alias: Optional[str]) -> QuerySet[_T]: ...
@property
def ordered(self) -> bool: ...
class Manager(BaseManager[_T]): ...

View File

@@ -28,9 +28,10 @@ from django.db.models.query_utils import Q as Q # noqa: F401
from django.db.models.sql.query import Query, RawQuery
_T = TypeVar("_T", bound=models.Model, covariant=True)
_QS = TypeVar("_QS", bound="QuerySet")
_Row = TypeVar("_Row", covariant=True)
_QS = TypeVar("_QS", bound="_QuerySet")
class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
class _QuerySet(Generic[_T, _Row], Collection[_Row], Reversible[_Row], Sized):
model: Type[_T]
query: Query
def __init__(
@@ -47,11 +48,13 @@ class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
def __class_getitem__(cls: Type[_QS], item: Type[_T]) -> Type[_QS]: ...
def __getstate__(self) -> Dict[str, Any]: ...
# Technically, the other QuerySet must be of the same type _T, but _T is covariant
def __and__(self: _QS, other: QuerySet[_T]) -> _QS: ...
def __or__(self: _QS, other: QuerySet[_T]) -> _QS: ...
def iterator(self, chunk_size: int = ...) -> Iterator[_T]: ...
def __and__(self: _QS, other: _QuerySet[_T, _Row]) -> _QS: ...
def __or__(self: _QS, other: _QuerySet[_T, _Row]) -> _QS: ...
# IMPORTANT: When updating any of the following methods' signatures, please ALSO modify
# the corresponding method in BaseManager.
def iterator(self, chunk_size: int = ...) -> Iterator[_Row]: ...
def aggregate(self, *args: Any, **kwargs: Any) -> Dict[str, Any]: ...
def get(self, *args: Any, **kwargs: Any) -> _T: ...
def get(self, *args: Any, **kwargs: Any) -> _Row: ...
def create(self, *args: Any, **kwargs: Any) -> _T: ...
def bulk_create(
self, objs: Iterable[_T], batch_size: Optional[int] = ..., ignore_conflicts: bool = ...
@@ -61,10 +64,10 @@ class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
def update_or_create(
self, defaults: Optional[MutableMapping[str, Any]] = ..., **kwargs: Any
) -> Tuple[_T, bool]: ...
def earliest(self, *fields: Any, field_name: Optional[Any] = ...) -> _T: ...
def latest(self, *fields: Any, field_name: Optional[Any] = ...) -> _T: ...
def first(self) -> Optional[_T]: ...
def last(self) -> Optional[_T]: ...
def earliest(self, *fields: Any, field_name: Optional[Any] = ...) -> _Row: ...
def latest(self, *fields: Any, field_name: Optional[Any] = ...) -> _Row: ...
def first(self) -> Optional[_Row]: ...
def last(self) -> Optional[_Row]: ...
def in_bulk(self, id_list: Iterable[Any] = ..., *, field_name: str = ...) -> Dict[Any, _T]: ...
def delete(self) -> Tuple[int, Dict[str, int]]: ...
def update(self, **kwargs: Any) -> int: ...
@@ -78,15 +81,15 @@ class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
using: Optional[str] = ...,
) -> RawQuerySet: ...
# The type of values may be overridden to be more specific in the mypy plugin, depending on the fields param
def values(self, *fields: Union[str, Combinable], **expressions: Any) -> _ValuesQuerySet[_T, Dict[str, Any]]: ...
def values(self, *fields: Union[str, Combinable], **expressions: Any) -> _QuerySet[_T, Dict[str, Any]]: ...
# The type of values_list may be overridden to be more specific in the mypy plugin, depending on the fields param
def values_list(
self, *fields: Union[str, Combinable], flat: bool = ..., named: bool = ...
) -> _ValuesQuerySet[_T, Any]: ...
def dates(self, field_name: str, kind: str, order: str = ...) -> _ValuesQuerySet[_T, datetime.date]: ...
) -> _QuerySet[_T, Any]: ...
def dates(self, field_name: str, kind: str, order: str = ...) -> _QuerySet[_T, datetime.date]: ...
def datetimes(
self, field_name: str, kind: str, order: str = ..., tzinfo: Optional[datetime.tzinfo] = ...
) -> _ValuesQuerySet[_T, datetime.datetime]: ...
) -> _QuerySet[_T, datetime.datetime]: ...
def none(self: _QS) -> _QS: ...
def all(self: _QS) -> _QS: ...
def filter(self: _QS, *args: Any, **kwargs: Any) -> _QS: ...
@@ -101,8 +104,7 @@ class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
) -> _QS: ...
def select_related(self: _QS, *fields: Any) -> _QS: ...
def prefetch_related(self: _QS, *lookups: Any) -> _QS: ...
# TODO: return type
def annotate(self, *args: Any, **kwargs: Any) -> QuerySet[Any]: ...
def annotate(self: _QS, *args: Any, **kwargs: Any) -> _QS: ...
def alias(self: _QS, *args: Any, **kwargs: Any) -> _QS: ...
def order_by(self: _QS, *field_names: Any) -> _QS: ...
def distinct(self: _QS, *field_names: Any) -> _QS: ...
@@ -115,7 +117,7 @@ class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
tables: Optional[List[str]] = ...,
order_by: Optional[Sequence[str]] = ...,
select_params: Optional[Sequence[Any]] = ...,
) -> QuerySet[Any]: ...
) -> _QuerySet[Any, Any]: ...
def reverse(self: _QS) -> _QS: ...
def defer(self: _QS, *fields: Any) -> _QS: ...
def only(self: _QS, *fields: Any) -> _QS: ...
@@ -125,36 +127,13 @@ class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
@property
def db(self) -> str: ...
def resolve_expression(self, *args: Any, **kwargs: Any) -> Any: ...
def __iter__(self) -> Iterator[_T]: ...
def __contains__(self, x: object) -> bool: ...
@overload
def __getitem__(self, i: int) -> _T: ...
@overload
def __getitem__(self: _QS, s: slice) -> _QS: ...
def __reversed__(self) -> Iterator[_T]: ...
_Row = TypeVar("_Row", covariant=True)
class _ValuesQuerySet(Generic[_T, _Row], Collection[_Row], Reversible[_Row], QuerySet[_T], Sized): # type: ignore
def __len__(self) -> int: ...
def __contains__(self, x: object) -> bool: ...
def __iter__(self) -> Iterator[_Row]: ...
def __contains__(self, x: object) -> bool: ...
@overload
def __getitem__(self, i: int) -> _Row: ...
@overload
def __getitem__(self: _QS, s: slice) -> _QS: ... # type: ignore
def iterator(self, chunk_size: int = ...) -> Iterator[_Row]: ...
def get(self, *args: Any, **kwargs: Any) -> _Row: ...
def earliest(self, *fields: Any, field_name: Optional[Any] = ...) -> _Row: ...
def latest(self, *fields: Any, field_name: Optional[Any] = ...) -> _Row: ...
def first(self) -> Optional[_Row]: ...
def last(self) -> Optional[_Row]: ...
def distinct(self, *field_names: Any) -> _ValuesQuerySet[_T, _Row]: ...
def order_by(self, *field_names: Any) -> _ValuesQuerySet[_T, _Row]: ...
def all(self) -> _ValuesQuerySet[_T, _Row]: ...
def annotate(self, *args: Any, **kwargs: Any) -> _ValuesQuerySet[_T, Any]: ...
def filter(self, *args: Any, **kwargs: Any) -> _ValuesQuerySet[_T, _Row]: ...
def exclude(self, *args: Any, **kwargs: Any) -> _ValuesQuerySet[_T, _Row]: ...
def __getitem__(self: _QS, s: slice) -> _QS: ...
def __reversed__(self) -> Iterator[_Row]: ...
class RawQuerySet(Iterable[_T], Sized):
query: RawQuery
@@ -188,6 +167,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]: ...
QuerySet = _QuerySet[_T, _T]
class Prefetch(object):
def __init__(self, lookup: str, queryset: Optional[QuerySet] = ..., to_attr: Optional[str] = ...) -> None: ...
def __getstate__(self) -> Dict[str, Any]: ...

View File

@@ -6,7 +6,7 @@ from django.db.models.query import QuerySet
from django.http import HttpRequest, HttpResponse
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
T = TypeVar("T", bound=Model)
T = TypeVar("T", bound=Model, covariant=True)
class MultipleObjectMixin(Generic[T], ContextMixin):
allow_empty: bool = ...

View File

@@ -1,4 +1,6 @@
from .aliases import ValuesQuerySet as ValuesQuerySet
from .annotations import Annotations as Annotations
from .annotations import WithAnnotations as WithAnnotations
from .patch import monkeypatch as monkeypatch
__all__ = ["monkeypatch", "ValuesQuerySet"]
__all__ = ["monkeypatch", "ValuesQuerySet", "WithAnnotations", "Annotations"]

View File

@@ -1,8 +1,10 @@
import typing
if typing.TYPE_CHECKING:
from django.db.models.query import _T, _Row, _ValuesQuerySet
from django.db.models.query import _T, _QuerySet, _Row
ValuesQuerySet = _ValuesQuerySet[_T, _Row]
ValuesQuerySet = _QuerySet[_T, _Row]
else:
ValuesQuerySet = typing.Any
from django.db.models.query import QuerySet
ValuesQuerySet = QuerySet

View File

@@ -0,0 +1,22 @@
from typing import Any, Generic, Mapping, TypeVar
from django.db.models.base import Model
from typing_extensions import Annotated
# Really, we would like to use TypedDict as a bound, but it's not possible
_Annotations = TypeVar("_Annotations", covariant=True, bound=Mapping[str, Any])
class Annotations(Generic[_Annotations]):
"""Use as `Annotations[MyTypedDict]`"""
pass
_T = TypeVar("_T", bound=Model)
WithAnnotations = Annotated[_T, Annotations[_Annotations]]
"""Alias to make it easy to annotate the model `_T` as having annotations `_Annotations` (a `TypedDict` or `Any` if not provided).
Use as `WithAnnotations[MyModel]` or `WithAnnotations[MyModel, MyTypedDict]`.
"""

View File

@@ -0,0 +1,8 @@
from typing import Any
from django_stubs_ext import ValuesQuerySet
def test_extends_values_queryset() -> None:
class MyQS(ValuesQuerySet[Any, Any]):
pass

View File

@@ -21,6 +21,7 @@ from mypy.types import Type as MypyType
from mypy.types import TypeOfAny, UnionType
from mypy_django_plugin.lib import fullnames, helpers
from mypy_django_plugin.lib.fullnames import WITH_ANNOTATIONS_FULLNAME
try:
from django.contrib.postgres.fields import ArrayField
@@ -113,7 +114,15 @@ class DjangoContext:
return modules
def get_model_class_by_fullname(self, fullname: str) -> Optional[Type[Model]]:
# Returns None if Model is abstract
"""Returns None if Model is abstract"""
annotated_prefix = WITH_ANNOTATIONS_FULLNAME + "["
if fullname.startswith(annotated_prefix):
# For our "annotated models", extract the original model fullname
fullname = fullname[len(annotated_prefix) :].rstrip("]")
if "," in fullname:
# Remove second type arg, which might be present
fullname = fullname[: fullname.index(",")]
module, _, model_cls_name = fullname.rpartition(".")
for model_cls in self.model_modules.get(module, set()):
if model_cls.__name__ == model_cls_name:

View File

@@ -11,11 +11,14 @@ ONETOONE_FIELD_FULLNAME = "django.db.models.fields.related.OneToOneField"
MANYTOMANY_FIELD_FULLNAME = "django.db.models.fields.related.ManyToManyField"
DUMMY_SETTINGS_BASE_CLASS = "django.conf._DjangoConfLazyObject"
QUERYSET_CLASS_FULLNAME = "django.db.models.query.QuerySet"
QUERYSET_CLASS_FULLNAME = "django.db.models.query._QuerySet"
BASE_MANAGER_CLASS_FULLNAME = "django.db.models.manager.BaseManager"
MANAGER_CLASS_FULLNAME = "django.db.models.manager.Manager"
RELATED_MANAGER_CLASS = "django.db.models.manager.RelatedManager"
WITH_ANNOTATIONS_FULLNAME = "django_stubs_ext.WithAnnotations"
ANNOTATIONS_FULLNAME = "django_stubs_ext.annotations.Annotations"
BASEFORM_CLASS_FULLNAME = "django.forms.forms.BaseForm"
FORM_CLASS_FULLNAME = "django.forms.forms.Form"
MODELFORM_CLASS_FULLNAME = "django.forms.models.ModelForm"
@@ -34,3 +37,5 @@ OPTIONS_CLASS_FULLNAME = "django.db.models.options.Options"
HTTPREQUEST_CLASS_FULLNAME = "django.http.request.HttpRequest"
F_EXPRESSION_FULLNAME = "django.db.models.expressions.F"
ANY_ATTR_ALLOWED_CLASS_FULLNAME = "django._AnyAttrAllowed"

View File

@@ -41,6 +41,7 @@ from mypy.types import Type as MypyType
from mypy.types import TypedDictType, TypeOfAny, UnionType
from mypy_django_plugin.lib import fullnames
from mypy_django_plugin.lib.fullnames import WITH_ANNOTATIONS_FULLNAME
if TYPE_CHECKING:
from mypy_django_plugin.django.context import DjangoContext
@@ -61,7 +62,15 @@ def is_toml(filename: str) -> bool:
def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]:
if "." not in fullname:
return None
module, cls_name = fullname.rsplit(".", 1)
if "[" in fullname and "]" in fullname:
# We sometimes generate fake fullnames like a.b.C[x.y.Z] to provide a better representation to users
# Make sure that we handle lookups of those types of names correctly if the part inside [] contains "."
bracket_start = fullname.index("[")
fullname_without_bracket = fullname[:bracket_start]
module, cls_name = fullname_without_bracket.rsplit(".", 1)
cls_name += fullname[bracket_start:]
else:
module, cls_name = fullname.rsplit(".", 1)
module_file = all_modules.get(module)
if module_file is None:
@@ -195,6 +204,10 @@ def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]
return None
def is_annotated_model_fullname(model_cls_fullname: str) -> bool:
return model_cls_fullname.startswith(WITH_ANNOTATIONS_FULLNAME + "[")
def add_new_class_for_module(
module: MypyFile, name: str, bases: List[Instance], fields: Optional[Dict[str, MypyType]] = None
) -> TypeInfo:
@@ -233,10 +246,14 @@ def get_current_module(api: TypeChecker) -> MypyFile:
return current_module
def make_oneoff_named_tuple(api: TypeChecker, name: str, fields: "OrderedDict[str, MypyType]") -> TupleType:
def make_oneoff_named_tuple(
api: TypeChecker, name: str, fields: "OrderedDict[str, MypyType]", extra_bases: Optional[List[Instance]] = None
) -> TupleType:
current_module = get_current_module(api)
if extra_bases is None:
extra_bases = []
namedtuple_info = add_new_class_for_module(
current_module, name, bases=[api.named_generic_type("typing.NamedTuple", [])], fields=fields
current_module, name, bases=[api.named_generic_type("typing.NamedTuple", [])] + extra_bases, fields=fields
)
return TupleType(list(fields.values()), fallback=Instance(namedtuple_info, []))
@@ -373,14 +390,8 @@ def copy_method_to_another_class(
for arg_name, arg_type, original_argument in zip(
method_type.arg_names[1:], method_type.arg_types[1:], original_arguments
):
bound_arg_type = semanal_api.anal_type(arg_type, allow_placeholder=True)
if bound_arg_type is None and not semanal_api.final_iteration:
semanal_api.defer()
return
assert bound_arg_type is not None
if isinstance(bound_arg_type, PlaceholderNode):
bound_arg_type = semanal_api.anal_type(arg_type)
if bound_arg_type is None:
return
var = Var(name=original_argument.variable.name, type=arg_type)

View File

@@ -10,6 +10,7 @@ from mypy.modulefinder import mypy_path
from mypy.nodes import MypyFile, TypeInfo
from mypy.options import Options
from mypy.plugin import (
AnalyzeTypeContext,
AttributeContext,
ClassDefContext,
DynamicClassDefContext,
@@ -24,7 +25,11 @@ from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers
from mypy_django_plugin.transformers import fields, forms, init_create, meta, querysets, request, settings
from mypy_django_plugin.transformers.managers import create_new_manager_class_from_from_queryset_method
from mypy_django_plugin.transformers.models import process_model_class, set_auth_user_model_boolean_fields
from mypy_django_plugin.transformers.models import (
handle_annotated_type,
process_model_class,
set_auth_user_model_boolean_fields,
)
def transform_model_class(ctx: ClassDefContext, django_context: DjangoContext) -> None:
@@ -230,7 +235,7 @@ class NewSemanalDjangoPlugin(Plugin):
related_model_module = related_model_cls.__module__
if related_model_module != file.fullname:
deps.add(self._new_dependency(related_model_module))
return list(deps)
return list(deps) + [self._new_dependency("django_stubs_ext")] # for annotate
def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], MypyType]]:
if fullname == "django.contrib.auth.get_user_model":
@@ -261,22 +266,28 @@ class NewSemanalDjangoPlugin(Plugin):
if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
return forms.extract_proper_type_for_get_form
manager_classes = self._get_current_manager_bases()
if method_name == "values":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME) or class_fullname in manager_classes:
return partial(querysets.extract_proper_type_queryset_values, django_context=self.django_context)
if method_name == "values_list":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME):
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME) or class_fullname in manager_classes:
return partial(querysets.extract_proper_type_queryset_values_list, django_context=self.django_context)
if method_name == "annotate":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.QUERYSET_CLASS_FULLNAME) or class_fullname in manager_classes:
return partial(querysets.extract_proper_type_queryset_annotate, django_context=self.django_context)
if method_name == "get_field":
info = self._get_typeinfo_or_none(class_fullname)
if info and info.has_base(fullnames.OPTIONS_CLASS_FULLNAME):
return partial(meta.return_proper_field_type_from_get_field, django_context=self.django_context)
manager_classes = self._get_current_manager_bases()
if class_fullname in manager_classes and method_name == "create":
return partial(init_create.redefine_and_typecheck_model_create, django_context=self.django_context)
if class_fullname in manager_classes and method_name in {"filter", "get", "exclude"}:
@@ -314,6 +325,14 @@ class NewSemanalDjangoPlugin(Plugin):
return partial(set_auth_user_model_boolean_fields, django_context=self.django_context)
return None
def get_type_analyze_hook(self, fullname: str) -> Optional[Callable[[AnalyzeTypeContext], MypyType]]:
if fullname in (
"typing.Annotated",
"typing_extensions.Annotated",
"django_stubs_ext.annotations.WithAnnotations",
):
return partial(handle_annotated_type, django_context=self.django_context)
def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicClassDefContext], None]]:
if fullname.endswith("from_queryset"):
class_name, _, _ = fullname.rpartition(".")

View File

@@ -19,6 +19,16 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
return
assert isinstance(base_manager_info, TypeInfo)
passed_queryset = ctx.call.args[0]
assert isinstance(passed_queryset, NameExpr)
derived_queryset_fullname = passed_queryset.fullname
if derived_queryset_fullname is None:
# In some cases, due to the way the semantic analyzer works, only passed_queryset.name is available.
# But it should be analyzed again, so this isn't a problem.
return
new_manager_info = semanal_api.basic_new_typeinfo(
ctx.name, basetype_or_fallback=Instance(base_manager_info, [AnyType(TypeOfAny.unannotated)]), line=ctx.call.line
)
@@ -28,11 +38,6 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
current_module = semanal_api.cur_mod_node
current_module.names[ctx.name] = SymbolTableNode(GDEF, new_manager_info, plugin_generated=True)
passed_queryset = ctx.call.args[0]
assert isinstance(passed_queryset, NameExpr)
derived_queryset_fullname = passed_queryset.fullname
assert derived_queryset_fullname is not None
sym = semanal_api.lookup_fully_qualified_or_none(derived_queryset_fullname)
assert sym is not None

View File

@@ -1,19 +1,22 @@
from typing import Dict, List, Optional, Type, cast
from typing import Dict, List, Optional, Type, Union, cast
from django.db.models.base import Model
from django.db.models.fields import DateField, DateTimeField
from django.db.models.fields.related import ForeignKey
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
from mypy.checker import TypeChecker
from mypy.nodes import ARG_STAR2, Argument, Context, FuncDef, TypeInfo, Var
from mypy.plugin import AttributeContext, ClassDefContext
from mypy.plugin import AnalyzeTypeContext, AttributeContext, CheckerPluginInterface, ClassDefContext
from mypy.plugins import common
from mypy.semanal import SemanticAnalyzer
from mypy.types import AnyType, Instance
from mypy.types import Type as MypyType
from mypy.types import TypeOfAny
from mypy.types import TypedDictType, TypeOfAny
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers
from mypy_django_plugin.lib.fullnames import ANNOTATIONS_FULLNAME, ANY_ATTR_ALLOWED_CLASS_FULLNAME
from mypy_django_plugin.lib.helpers import add_new_class_for_module
from mypy_django_plugin.transformers import fields
from mypy_django_plugin.transformers.fields import get_field_descriptor_types
@@ -194,7 +197,6 @@ class AddManagers(ModelClassInitializer):
for manager_name, manager in model_cls._meta.managers_map.items():
manager_class_name = manager.__class__.__name__
manager_fullname = helpers.get_class_fullname(manager.__class__)
try:
manager_info = self.lookup_typeinfo_or_incomplete_defn_error(manager_fullname)
except helpers.IncompleteDefnException as exc:
@@ -390,3 +392,76 @@ def set_auth_user_model_boolean_fields(ctx: AttributeContext, django_context: Dj
boolinfo = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), bool)
assert boolinfo is not None
return Instance(boolinfo, [])
def handle_annotated_type(ctx: AnalyzeTypeContext, django_context: DjangoContext) -> MypyType:
args = ctx.type.args
type_arg = ctx.api.analyze_type(args[0])
api = cast(SemanticAnalyzer, ctx.api.api) # type: ignore
if not isinstance(type_arg, Instance):
return ctx.api.analyze_type(ctx.type)
fields_dict = None
if len(args) > 1:
second_arg_type = ctx.api.analyze_type(args[1])
if isinstance(second_arg_type, TypedDictType):
fields_dict = second_arg_type
elif isinstance(second_arg_type, Instance) and second_arg_type.type.fullname == ANNOTATIONS_FULLNAME:
annotations_type_arg = second_arg_type.args[0]
if isinstance(annotations_type_arg, TypedDictType):
fields_dict = annotations_type_arg
elif not isinstance(annotations_type_arg, AnyType):
ctx.api.fail("Only TypedDicts are supported as type arguments to Annotations", ctx.context)
return get_or_create_annotated_type(api, type_arg, fields_dict=fields_dict)
def get_or_create_annotated_type(
api: Union[SemanticAnalyzer, CheckerPluginInterface], model_type: Instance, fields_dict: Optional[TypedDictType]
) -> Instance:
"""
Get or create the type for a model for which you getting/setting any attr is allowed.
The generated type is an subclass of the model and django._AnyAttrAllowed.
The generated type is placed in the django_stubs_ext module, with the name WithAnnotations[ModelName].
If the user wanted to annotate their code using this type, then this is the annotation they would use.
This is a bit of a hack to make a pretty type for error messages and which would make sense for users.
"""
model_module_name = "django_stubs_ext"
if helpers.is_annotated_model_fullname(model_type.type.fullname):
# If it's already a generated class, we want to use the original model as a base
model_type = model_type.type.bases[0]
if fields_dict is not None:
type_name = f"WithAnnotations[{model_type.type.fullname}, {fields_dict}]"
else:
type_name = f"WithAnnotations[{model_type.type.fullname}]"
annotated_typeinfo = helpers.lookup_fully_qualified_typeinfo(
cast(TypeChecker, api), model_module_name + "." + type_name
)
if annotated_typeinfo is None:
model_module_file = api.modules[model_module_name] # type: ignore
if isinstance(api, SemanticAnalyzer):
annotated_model_type = api.named_type_or_none(ANY_ATTR_ALLOWED_CLASS_FULLNAME, [])
assert annotated_model_type is not None
else:
annotated_model_type = api.named_generic_type(ANY_ATTR_ALLOWED_CLASS_FULLNAME, [])
annotated_typeinfo = add_new_class_for_module(
model_module_file,
type_name,
bases=[model_type] if fields_dict is not None else [model_type, annotated_model_type],
fields=fields_dict.items if fields_dict is not None else None,
)
if fields_dict is not None:
# To allow structural subtyping, make it a Protocol
annotated_typeinfo.is_protocol = True
# Save for later to easily find which field types were annotated
annotated_typeinfo.metadata["annotated_field_types"] = fields_dict.items
annotated_type = Instance(annotated_typeinfo, [])
return annotated_type

View File

@@ -5,6 +5,7 @@ from mypy.types import TypeOfAny
from mypy_django_plugin.django.context import DjangoContext
from mypy_django_plugin.lib import fullnames, helpers
from mypy_django_plugin.lib.helpers import is_annotated_model_fullname
def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
@@ -29,7 +30,11 @@ def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext)
):
provided_type = resolve_combinable_type(provided_type, django_context)
lookup_type = django_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg)
lookup_type: MypyType
if is_annotated_model_fullname(model_cls_fullname):
lookup_type = AnyType(TypeOfAny.implementation_artifact)
else:
lookup_type = django_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg)
# Managers as provided_type is not supported yet
if isinstance(provided_type, Instance) and helpers.has_any_of_bases(
provided_type.type, (fullnames.MANAGER_CLASS_FULLNAME, fullnames.QUERYSET_CLASS_FULLNAME)

View File

@@ -1,18 +1,21 @@
from collections import OrderedDict
from typing import List, Optional, Sequence, Type
from typing import Dict, List, Optional, Sequence, Type
from django.core.exceptions import FieldError
from django.db.models.base import Model
from django.db.models.fields.related import RelatedField
from django.db.models.fields.reverse_related import ForeignObjectRel
from mypy.nodes import Expression, NameExpr
from mypy.nodes import ARG_NAMED, ARG_NAMED_OPT, Expression, NameExpr
from mypy.plugin import FunctionContext, MethodContext
from mypy.types import AnyType, Instance
from mypy.types import AnyType, Instance, TupleType
from mypy.types import Type as MypyType
from mypy.types import TypeOfAny
from mypy.types import TypedDictType, TypeOfAny, get_proper_type
from mypy_django_plugin.django.context import DjangoContext, LookupsAreUnsupported
from mypy_django_plugin.lib import fullnames, helpers
from mypy_django_plugin.lib.fullnames import ANY_ATTR_ALLOWED_CLASS_FULLNAME
from mypy_django_plugin.lib.helpers import is_annotated_model_fullname
from mypy_django_plugin.transformers.models import get_or_create_annotated_type
def _extract_model_type_from_queryset(queryset_type: Instance) -> Optional[Instance]:
@@ -38,12 +41,19 @@ def determine_proper_manager_type(ctx: FunctionContext) -> MypyType:
def get_field_type_from_lookup(
ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model], *, method: str, lookup: str
ctx: MethodContext,
django_context: DjangoContext,
model_cls: Type[Model],
*,
method: str,
lookup: str,
silent_on_error: bool = False,
) -> Optional[MypyType]:
try:
lookup_field = django_context.resolve_lookup_into_field(model_cls, lookup)
except FieldError as exc:
ctx.api.fail(exc.args[0], ctx.context)
if not silent_on_error:
ctx.api.fail(exc.args[0], ctx.context)
return None
except LookupsAreUnsupported:
return AnyType(TypeOfAny.explicit)
@@ -61,7 +71,13 @@ def get_field_type_from_lookup(
def get_values_list_row_type(
ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model], flat: bool, named: bool
ctx: MethodContext,
django_context: DjangoContext,
model_cls: Type[Model],
*,
is_annotated: bool,
flat: bool,
named: bool,
) -> MypyType:
field_lookups = resolve_field_lookups(ctx.args[0], django_context)
if field_lookups is None:
@@ -81,9 +97,20 @@ def get_values_list_row_type(
for field in django_context.get_model_fields(model_cls):
column_type = django_context.get_field_get_type(typechecker_api, field, method="values_list")
column_types[field.attname] = column_type
return helpers.make_oneoff_named_tuple(typechecker_api, "Row", column_types)
if is_annotated:
# Return a NamedTuple with a fallback so that it's possible to access any field
return helpers.make_oneoff_named_tuple(
typechecker_api,
"Row",
column_types,
extra_bases=[typechecker_api.named_generic_type(ANY_ATTR_ALLOWED_CLASS_FULLNAME, [])],
)
else:
return helpers.make_oneoff_named_tuple(typechecker_api, "Row", column_types)
else:
# flat=False, named=False, all fields
if is_annotated:
return typechecker_api.named_generic_type("builtins.tuple", [AnyType(TypeOfAny.special_form)])
field_lookups = []
for field in django_context.get_model_fields(model_cls):
field_lookups.append(field.attname)
@@ -95,10 +122,13 @@ def get_values_list_row_type(
column_types = OrderedDict()
for field_lookup in field_lookups:
lookup_field_type = get_field_type_from_lookup(
ctx, django_context, model_cls, lookup=field_lookup, method="values_list"
ctx, django_context, model_cls, lookup=field_lookup, method="values_list", silent_on_error=is_annotated
)
if lookup_field_type is None:
return AnyType(TypeOfAny.from_error)
if is_annotated:
lookup_field_type = AnyType(TypeOfAny.from_omitted_generics)
else:
return AnyType(TypeOfAny.from_error)
column_types[field_lookup] = lookup_field_type
if flat:
@@ -115,7 +145,8 @@ def get_values_list_row_type(
def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
# called on the Instance, returns QuerySet of something
assert isinstance(ctx.type, Instance)
assert isinstance(ctx.default_return_type, Instance)
default_return_type = get_proper_type(ctx.default_return_type)
assert isinstance(default_return_type, Instance)
model_type = _extract_model_type_from_queryset(ctx.type)
if model_type is None:
@@ -123,7 +154,7 @@ def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context:
model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname)
if model_cls is None:
return ctx.default_return_type
return default_return_type
flat_expr = helpers.get_call_argument_by_name(ctx, "flat")
if flat_expr is not None and isinstance(flat_expr, NameExpr):
@@ -139,14 +170,89 @@ def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context:
if flat and named:
ctx.api.fail("'flat' and 'named' can't be used together", ctx.context)
return helpers.reparametrize_instance(ctx.default_return_type, [model_type, AnyType(TypeOfAny.from_error)])
return helpers.reparametrize_instance(default_return_type, [model_type, AnyType(TypeOfAny.from_error)])
# account for possible None
flat = flat or False
named = named or False
row_type = get_values_list_row_type(ctx, django_context, model_cls, flat=flat, named=named)
return helpers.reparametrize_instance(ctx.default_return_type, [model_type, row_type])
is_annotated = is_annotated_model_fullname(model_type.type.fullname)
row_type = get_values_list_row_type(
ctx, django_context, model_cls, is_annotated=is_annotated, flat=flat, named=named
)
return helpers.reparametrize_instance(default_return_type, [model_type, row_type])
def gather_kwargs(ctx: MethodContext) -> Optional[Dict[str, MypyType]]:
num_args = len(ctx.arg_kinds)
kwargs = {}
named = (ARG_NAMED, ARG_NAMED_OPT)
for i in range(num_args):
if not ctx.arg_kinds[i]:
continue
if any(kind not in named for kind in ctx.arg_kinds[i]):
# Only named arguments supported
return None
for j in range(len(ctx.arg_names[i])):
name = ctx.arg_names[i][j]
assert name is not None
kwargs[name] = ctx.arg_types[i][j]
return kwargs
def extract_proper_type_queryset_annotate(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
# called on the Instance, returns QuerySet of something
assert isinstance(ctx.type, Instance)
default_return_type = get_proper_type(ctx.default_return_type)
assert isinstance(default_return_type, Instance)
model_type = _extract_model_type_from_queryset(ctx.type)
if model_type is None:
return AnyType(TypeOfAny.from_omitted_generics)
api = ctx.api
field_types = model_type.type.metadata.get("annotated_field_types")
kwargs = gather_kwargs(ctx)
if kwargs:
# For now, we don't try to resolve the output_field of the field would be, but use Any.
added_field_types = {name: AnyType(TypeOfAny.implementation_artifact) for name, typ in kwargs.items()}
if field_types is not None:
# Annotate was called more than once, so add/update existing field types
field_types.update(added_field_types)
else:
field_types = added_field_types
fields_dict = None
if field_types is not None:
fields_dict = helpers.make_typeddict(
api, fields=OrderedDict(field_types), required_keys=set(field_types.keys())
)
annotated_type = get_or_create_annotated_type(api, model_type, fields_dict=fields_dict)
row_type: MypyType
if len(default_return_type.args) > 1:
original_row_type: MypyType = default_return_type.args[1]
row_type = original_row_type
if isinstance(original_row_type, TypedDictType):
row_type = api.named_generic_type(
"builtins.dict", [api.named_generic_type("builtins.str", []), AnyType(TypeOfAny.from_omitted_generics)]
)
elif isinstance(original_row_type, TupleType):
fallback: Instance = original_row_type.partial_fallback
if fallback is not None and fallback.type.has_base("typing.NamedTuple"):
# TODO: Use a NamedTuple which contains the known fields, but also
# falls back to allowing any attribute access.
row_type = AnyType(TypeOfAny.implementation_artifact)
else:
row_type = api.named_generic_type("builtins.tuple", [AnyType(TypeOfAny.from_omitted_generics)])
elif isinstance(original_row_type, Instance) and original_row_type.type.has_base(
fullnames.MODEL_CLASS_FULLNAME
):
row_type = annotated_type
else:
row_type = annotated_type
return helpers.reparametrize_instance(default_return_type, [annotated_type, row_type])
def resolve_field_lookups(lookup_exprs: Sequence[Expression], django_context: DjangoContext) -> Optional[List[str]]:
@@ -162,7 +268,8 @@ def resolve_field_lookups(lookup_exprs: Sequence[Expression], django_context: Dj
def extract_proper_type_queryset_values(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
# called on QuerySet, return QuerySet of something
assert isinstance(ctx.type, Instance)
assert isinstance(ctx.default_return_type, Instance)
default_return_type = get_proper_type(ctx.default_return_type)
assert isinstance(default_return_type, Instance)
model_type = _extract_model_type_from_queryset(ctx.type)
if model_type is None:
@@ -170,7 +277,10 @@ def extract_proper_type_queryset_values(ctx: MethodContext, django_context: Djan
model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname)
if model_cls is None:
return ctx.default_return_type
return default_return_type
if is_annotated_model_fullname(model_type.type.fullname):
return default_return_type
field_lookups = resolve_field_lookups(ctx.args[0], django_context)
if field_lookups is None:
@@ -186,9 +296,9 @@ def extract_proper_type_queryset_values(ctx: MethodContext, django_context: Djan
ctx, django_context, model_cls, lookup=field_lookup, method="values"
)
if field_lookup_type is None:
return helpers.reparametrize_instance(ctx.default_return_type, [model_type, AnyType(TypeOfAny.from_error)])
return helpers.reparametrize_instance(default_return_type, [model_type, AnyType(TypeOfAny.from_error)])
column_types[field_lookup] = field_lookup_type
row_type = helpers.make_typeddict(ctx.api, column_types, set(column_types.keys()))
return helpers.reparametrize_instance(ctx.default_return_type, [model_type, row_type])
return helpers.reparametrize_instance(default_return_type, [model_type, row_type])

View File

@@ -135,7 +135,7 @@
pass
class A(admin.ModelAdmin):
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin[Any], HttpRequest, QuerySet[Any]], None], str]"
actions = [an_action] # E: List item 0 has incompatible type "Callable[[None], None]"; expected "Union[Callable[[ModelAdmin[Any], HttpRequest, _QuerySet[Any, Any]], None], str]"
- case: errors_for_invalid_model_admin_generic
main: |
from django.contrib.admin import ModelAdmin

View File

@@ -0,0 +1,349 @@
- case: annotate_using_with_annotations
main: |
from typing_extensions import Annotated
from myapp.models import User
from django_stubs_ext import WithAnnotations, Annotations
from django.db.models.expressions import Value
annotated_user = User.objects.annotate(foo=Value("")).get()
unannotated_user = User.objects.get(id=1)
print(annotated_user.asdf) # E: "WithAnnotations[myapp.models.User, TypedDict({'foo': Any})]" has no attribute "asdf"
print(unannotated_user.asdf) # E: "User" has no attribute "asdf"
def func(user: Annotated[User, Annotations]) -> str:
return user.asdf
func(unannotated_user) # E: Argument 1 to "func" has incompatible type "User"; expected "WithAnnotations[myapp.models.User]"
func(annotated_user) # E: Argument 1 to "func" has incompatible type "WithAnnotations[myapp.models.User, TypedDict({'foo': Any})]"; expected "WithAnnotations[myapp.models.User]"
def func2(user: WithAnnotations[User]) -> str:
return user.asdf
func2(unannotated_user) # E: Argument 1 to "func2" has incompatible type "User"; expected "WithAnnotations[myapp.models.User]"
func2(annotated_user) # E: Argument 1 to "func2" has incompatible type "WithAnnotations[myapp.models.User, TypedDict({'foo': Any})]"; expected "WithAnnotations[myapp.models.User]"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
username = models.CharField(max_length=100)
- case: annotate_using_with_annotations_typeddict
main: |
from typing import Any
from typing_extensions import Annotated, TypedDict
from myapp.models import User
from django_stubs_ext import WithAnnotations, Annotations
from django.db.models.expressions import Value
class MyDict(TypedDict):
foo: str
def func(user: Annotated[User, Annotations[MyDict]]) -> str:
print(user.asdf) # E: "WithAnnotations[myapp.models.User, TypedDict('main.MyDict', {'foo': builtins.str})]" has no attribute "asdf"
return user.foo
unannotated_user = User.objects.get(id=1)
annotated_user = User.objects.annotate(foo=Value("")).get()
other_annotated_user = User.objects.annotate(other=Value("")).get()
func(unannotated_user) # E: Argument 1 to "func" has incompatible type "User"; expected "WithAnnotations[myapp.models.User, TypedDict('main.MyDict', {'foo': builtins.str})]"
x: WithAnnotations[User]
func(x)
func(annotated_user)
func(other_annotated_user) # E: Argument 1 to "func" has incompatible type "WithAnnotations[myapp.models.User, TypedDict({'other': Any})]"; expected "WithAnnotations[myapp.models.User, TypedDict('main.MyDict', {'foo': builtins.str})]"
def func2(user: WithAnnotations[User, MyDict]) -> str:
print(user.asdf) # E: "WithAnnotations[myapp.models.User, TypedDict('main.MyDict', {'foo': builtins.str})]" has no attribute "asdf"
return user.foo
func2(unannotated_user) # E: Argument 1 to "func2" has incompatible type "User"; expected "WithAnnotations[myapp.models.User, TypedDict('main.MyDict', {'foo': builtins.str})]"
func2(annotated_user)
func2(other_annotated_user) # E: Argument 1 to "func2" has incompatible type "WithAnnotations[myapp.models.User, TypedDict({'other': Any})]"; expected "WithAnnotations[myapp.models.User, TypedDict('main.MyDict', {'foo': builtins.str})]"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
username = models.CharField(max_length=100)
- case: annotate_using_with_annotations_typeddict_subtypes
main: |
from typing_extensions import Annotated, TypedDict
from myapp.models import User
from django_stubs_ext import WithAnnotations, Annotations
class BroadDict(TypedDict):
foo: str
bar: str
class NarrowDict(TypedDict):
foo: str
class OtherDict(TypedDict):
other: str
def func(user: WithAnnotations[User, NarrowDict]) -> str:
return user.foo
x: WithAnnotations[User, NarrowDict]
func(x)
y: WithAnnotations[User, BroadDict]
func(y)
z: WithAnnotations[User, OtherDict]
func(z) # E: Argument 1 to "func" has incompatible type "WithAnnotations[myapp.models.User, TypedDict('main.OtherDict', {'other': builtins.str})]"; expected "WithAnnotations[myapp.models.User, TypedDict('main.NarrowDict', {'foo': builtins.str})]"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
username = models.CharField(max_length=100)
- case: annotate_basic
main: |
from myapp.models import User
from django.db.models.expressions import F
qs = User.objects.annotate(foo=F('id'))
reveal_type(qs) # N: Revealed type is "django.db.models.query._QuerySet[django_stubs_ext.WithAnnotations[myapp.models.User, TypedDict({'foo': Any})], django_stubs_ext.WithAnnotations[myapp.models.User, TypedDict({'foo': Any})]]"
annotated = qs.get()
reveal_type(annotated) # N: Revealed type is "django_stubs_ext.WithAnnotations[myapp.models.User, TypedDict({'foo': Any})]*"
reveal_type(annotated.foo) # N: Revealed type is "Any"
print(annotated.bar) # E: "WithAnnotations[myapp.models.User, TypedDict({'foo': Any})]" has no attribute "bar"
reveal_type(annotated.username) # N: Revealed type is "builtins.str*"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
username = models.CharField(max_length=100)
- case: annotate_no_field_name
main: |
from myapp.models import User
from django.db.models import Count
qs = User.objects.annotate(Count('id'))
reveal_type(qs) # N: Revealed type is "django.db.models.query._QuerySet[django_stubs_ext.WithAnnotations[myapp.models.User], django_stubs_ext.WithAnnotations[myapp.models.User]]"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
username = models.CharField(max_length=100)
- case: annotate_model_usage_across_methods
main: |
from myapp.models import User, Animal
from django.db.models import Count
qs = User.objects.annotate(Count('id'))
annotated_user = qs.get()
def animals_only(param: Animal):
pass
# Make sure that even though attr access falls back to Any, the type is still checked
animals_only(annotated_user) # E: Argument 1 to "animals_only" has incompatible type "WithAnnotations[myapp.models.User]"; expected "Animal"
def users_allowed(param: User):
# But this function accepts only the original User type, so any attr access is not allowed within this function
param.foo # E: "User" has no attribute "foo"
# Passing in the annotated User to a function taking a (unannotated) User is OK
users_allowed(annotated_user)
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
username = models.CharField(max_length=100)
class Animal(models.Model):
barks = models.BooleanField()
- case: annotate_twice_works
main: |
from myapp.models import User
from django.db.models.expressions import F
# Django annotations are additive
qs = User.objects.annotate(foo=F('id'))
qs = qs.annotate(bar=F('id'))
annotated = qs.get()
reveal_type(annotated) # N: Revealed type is "django_stubs_ext.WithAnnotations[myapp.models.User, TypedDict({'foo': Any, 'bar': Any})]*"
reveal_type(annotated.foo) # N: Revealed type is "Any"
reveal_type(annotated.bar) # N: Revealed type is "Any"
reveal_type(annotated.username) # N: Revealed type is "builtins.str*"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
username = models.CharField(max_length=100)
- case: annotate_using_queryset_across_methods
main: |
from myapp.models import User
from django_stubs_ext import WithAnnotations
from django.db.models import QuerySet
from django.db.models.expressions import F
from typing_extensions import TypedDict
qs = User.objects.filter(id=1)
class FooDict(TypedDict):
foo: str
def add_annotation(qs: QuerySet[User]) -> QuerySet[WithAnnotations[User, FooDict]]:
return qs.annotate(foo=F('id'))
def add_wrong_annotation(qs: QuerySet[User]) -> QuerySet[WithAnnotations[User, FooDict]]:
return qs.annotate(bar=F('id')) # E: Incompatible return value type (got "_QuerySet[WithAnnotations[myapp.models.User, TypedDict({'bar': Any})], WithAnnotations[myapp.models.User, TypedDict({'bar': Any})]]", expected "_QuerySet[WithAnnotations[myapp.models.User, TypedDict('main.FooDict', {'foo': builtins.str})], WithAnnotations[myapp.models.User, TypedDict('main.FooDict', {'foo': builtins.str})]]")
qs = add_annotation(qs)
qs.get().foo
qs.get().bar # E: "WithAnnotations[myapp.models.User, TypedDict('main.FooDict', {'foo': builtins.str})]" has no attribute "bar"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
username = models.CharField(max_length=100)
- case: annotate_currently_allows_lookups_of_non_existant_field
main: |
from myapp.models import User
from django.db.models.expressions import F
User.objects.annotate(abc=F('id')).filter(abc=1).values_list()
# Invalid lookups are currently allowed after calling .annotate.
# It would be nice to in the future store the annotated names and use it when checking for valid lookups.
User.objects.annotate(abc=F('id')).filter(unknown_field=1).values_list()
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class User(models.Model):
pass
- case: annotate_values_or_values_list_before_or_after_annotate_broadens_type
main: |
from myapp.models import Blog
from django.db.models.expressions import F
values_list_double_annotate = Blog.objects.annotate(foo=F('id')).annotate(bar=F('id')).values_list('foo', 'bar').get()
reveal_type(values_list_double_annotate) # N: Revealed type is "Tuple[Any, Any]"
values_list_named = Blog.objects.annotate(foo=F('id'), bar=F('isad')).values_list('foo', 'text', named=True).get()
# We have to assume we don't know any of the tuple member types.
reveal_type(values_list_named) # N: Revealed type is "Tuple[Any, builtins.str, fallback=main.Row]"
values_list_named.unknown # E: "Row" has no attribute "unknown"
reveal_type(values_list_named.foo) # N: Revealed type is "Any"
reveal_type(values_list_named.text) # N: Revealed type is "builtins.str"
values_list_flat_known = Blog.objects.annotate(foo=F('id')).values_list('text', flat=True).get()
# Even though it's annotated, we still know the lookup's type.
reveal_type(values_list_flat_known) # N: Revealed type is "builtins.str*"
values_list_flat_unknown = Blog.objects.annotate(foo=F('id')).values_list('foo', flat=True).get()
# We don't know the type of an unknown lookup
reveal_type(values_list_flat_unknown) # N: Revealed type is "Any"
values_no_params = Blog.objects.annotate(foo=F('id')).values().get()
reveal_type(values_no_params) # N: Revealed type is "builtins.dict*[builtins.str, Any]"
values_list_no_params = Blog.objects.annotate(foo=F('id')).values_list().get()
reveal_type(values_list_no_params) # N: Revealed type is "builtins.tuple*[Any]"
values_list_flat_no_params = Blog.objects.annotate(foo=F('id')).values_list(flat=True).get()
reveal_type(values_list_flat_no_params) # N: Revealed type is "builtins.int*"
values_list_named_no_params = Blog.objects.annotate(foo=F('id')).values_list(named=True).get()
reveal_type(values_list_named_no_params.foo) # N: Revealed type is "Any"
reveal_type(values_list_named_no_params.text) # N: Revealed type is "builtins.str"
# .values/.values_list BEFORE .annotate
# The following should happen to the TypeVars:
# 1st typevar (Model): Blog => django_stubs_ext.WithAnnotations[Blog]
# 2nd typevar (Row): Should assume that we don't know what is in the row anymore (due to the annotation)
# Since we can't trust that only 'text' is in the row type anymore.
# It's possible to provide more precise types than than this, but without inspecting the
# arguments to .annotate, these are the best types we can infer.
qs1 = Blog.objects.values('text').annotate(foo=F('id'))
reveal_type(qs1) # N: Revealed type is "django.db.models.query._QuerySet[django_stubs_ext.WithAnnotations[myapp.models.Blog, TypedDict({'foo': Any})], builtins.dict[builtins.str, Any]]"
qs2 = Blog.objects.values_list('text').annotate(foo=F('id'))
reveal_type(qs2) # N: Revealed type is "django.db.models.query._QuerySet[django_stubs_ext.WithAnnotations[myapp.models.Blog, TypedDict({'foo': Any})], builtins.tuple[Any]]"
qs3 = Blog.objects.values_list('text', named=True).annotate(foo=F('id'))
# TODO: Would be nice to infer a NamedTuple which contains the field 'text' (str) + any number of other fields.
# The reason it would have to appear to have any other fields is that annotate could potentially be called with
# arbitrary parameters such that we wouldn't know how many extra fields there might be.
# But it's not trivial to make such a NamedTuple, partly because since it is also an ordinary tuple, it would
# have to have an arbitrary length, but still have certain fields at certain indices with specific types.
# For now, Any :)
reveal_type(qs3) # N: Revealed type is "django.db.models.query._QuerySet[django_stubs_ext.WithAnnotations[myapp.models.Blog, TypedDict({'foo': Any})], Any]"
qs4 = Blog.objects.values_list('text', flat=True).annotate(foo=F('id'))
reveal_type(qs4) # N: Revealed type is "django.db.models.query._QuerySet[django_stubs_ext.WithAnnotations[myapp.models.Blog, TypedDict({'foo': Any})], builtins.str]"
before_values_no_params = Blog.objects.values().annotate(foo=F('id')).get()
reveal_type(before_values_no_params) # N: Revealed type is "builtins.dict*[builtins.str, Any]"
before_values_list_no_params = Blog.objects.values_list().annotate(foo=F('id')).get()
reveal_type(before_values_list_no_params) # N: Revealed type is "builtins.tuple*[Any]"
before_values_list_flat_no_params = Blog.objects.values_list(flat=True).annotate(foo=F('id')).get()
reveal_type(before_values_list_flat_no_params) # N: Revealed type is "builtins.int*"
before_values_list_named_no_params = Blog.objects.values_list(named=True).annotate(foo=F('id')).get()
reveal_type(before_values_list_named_no_params.foo) # N: Revealed type is "Any"
# TODO: Would be nice to infer builtins.str:
reveal_type(before_values_list_named_no_params.text) # N: Revealed type is "Any"
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
content: |
from django.db import models
class Blog(models.Model):
num_posts = models.IntegerField()
text = models.CharField(max_length=100)

View File

@@ -3,22 +3,22 @@
from myapp.models import Blog
qs = Blog.objects.all()
reveal_type(qs) # N: Revealed type is "django.db.models.manager.Manager[myapp.models.Blog]"
reveal_type(qs) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Blog*, myapp.models.Blog*]"
reveal_type(qs.get(id=1)) # N: Revealed type is "myapp.models.Blog*"
reveal_type(iter(qs)) # N: Revealed type is "typing.Iterator[myapp.models.Blog*]"
reveal_type(qs.iterator()) # N: Revealed type is "typing.Iterator[myapp.models.Blog*]"
reveal_type(qs.first()) # N: Revealed type is "Union[myapp.models.Blog*, None]"
reveal_type(qs.earliest()) # N: Revealed type is "myapp.models.Blog*"
reveal_type(qs[0]) # N: Revealed type is "myapp.models.Blog*"
reveal_type(qs[:9]) # N: Revealed type is "django.db.models.manager.Manager[myapp.models.Blog]"
reveal_type(qs[:9]) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Blog, myapp.models.Blog]"
reveal_type(qs.in_bulk()) # N: Revealed type is "builtins.dict[Any, myapp.models.Blog*]"
# .dates / .datetimes
reveal_type(Blog.objects.dates("created_at", "day")) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Blog*, datetime.date]"
reveal_type(Blog.objects.datetimes("created_at", "day")) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Blog*, datetime.datetime]"
reveal_type(Blog.objects.dates("created_at", "day")) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Blog*, datetime.date]"
reveal_type(Blog.objects.datetimes("created_at", "day")) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Blog*, datetime.datetime]"
# AND-ing QuerySets
reveal_type(Blog.objects.all() & Blog.objects.all()) # N: Revealed type is "django.db.models.manager.Manager[myapp.models.Blog]"
reveal_type(Blog.objects.all() & Blog.objects.all()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Blog, myapp.models.Blog]"
installed_apps:
- myapp
files:

View File

@@ -111,8 +111,8 @@
- case: values_of_many_to_many_field
main: |
from myapp.models import Author, Book
reveal_type(Book.objects.values('authors')) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Book, TypedDict({'authors': builtins.int})]"
reveal_type(Author.objects.values('books')) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Author, TypedDict({'books': builtins.int})]"
reveal_type(Book.objects.values('authors')) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Book, TypedDict({'authors': builtins.int})]"
reveal_type(Author.objects.values('books')) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Author, TypedDict({'books': builtins.int})]"
installed_apps:
- myapp
files:

View File

@@ -37,7 +37,7 @@
reveal_type(query.all().get()) # N: Revealed type is "Tuple[builtins.str]"
reveal_type(query.filter(age__gt=16).get()) # N: Revealed type is "Tuple[builtins.str]"
reveal_type(query.exclude(age__lte=16).get()) # N: Revealed type is "Tuple[builtins.str]"
reveal_type(query.annotate(name_length=Length("name")).get()) # N: Revealed type is "Any"
reveal_type(query.annotate(name_length=Length("name")).get()) # N: Revealed type is "builtins.tuple*[Any]"
installed_apps:
- myapp
files:
@@ -214,8 +214,8 @@
- case: values_list_flat_true_with_ids
main: |
from myapp.models import Blog, Publisher
reveal_type(Blog.objects.values_list('id', flat=True)) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Blog, builtins.int]"
reveal_type(Blog.objects.values_list('publisher_id', flat=True)) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Blog, builtins.int]"
reveal_type(Blog.objects.values_list('id', flat=True)) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Blog, builtins.int]"
reveal_type(Blog.objects.values_list('publisher_id', flat=True)) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Blog, builtins.int]"
# is Iterable[int]
reveal_type(list(Blog.objects.values_list('id', flat=True))) # N: Revealed type is "builtins.list[builtins.int*]"
installed_apps:
@@ -234,8 +234,8 @@
main: |
from myapp.models import TransactionQuerySet
reveal_type(TransactionQuerySet()) # N: Revealed type is "myapp.models.TransactionQuerySet"
reveal_type(TransactionQuerySet().values()) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Transaction, TypedDict({'id': builtins.int, 'total': builtins.int})]"
reveal_type(TransactionQuerySet().values_list()) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Transaction, Tuple[builtins.int, builtins.int]]"
reveal_type(TransactionQuerySet().values()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Transaction, TypedDict({'id': builtins.int, 'total': builtins.int})]"
reveal_type(TransactionQuerySet().values_list()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Transaction, Tuple[builtins.int, builtins.int]]"
installed_apps:
- myapp
files:
@@ -251,8 +251,8 @@
- case: values_list_of_many_to_many_field
main: |
from myapp.models import Author, Book
reveal_type(Book.objects.values_list('authors')) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Book, Tuple[builtins.int]]"
reveal_type(Author.objects.values_list('books')) # N: Revealed type is "django.db.models.query._ValuesQuerySet[myapp.models.Author, Tuple[builtins.int]]"
reveal_type(Book.objects.values_list('authors')) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Book, Tuple[builtins.int]]"
reveal_type(Author.objects.values_list('books')) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.Author, Tuple[builtins.int]]"
installed_apps:
- myapp
files:

View File

@@ -308,14 +308,14 @@
main: |
from myapp.models import User
reveal_type(User.objects) # N: Revealed type is "myapp.models.User_MyManager2[myapp.models.User]"
reveal_type(User.objects.select_related()) # N: Revealed type is "myapp.models.User_MyManager2[myapp.models.User]"
reveal_type(User.objects.select_related()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.User*, myapp.models.User*]"
reveal_type(User.objects.get()) # N: Revealed type is "myapp.models.User*"
reveal_type(User.objects.get_instance()) # N: Revealed type is "builtins.int"
reveal_type(User.objects.get_instance_untyped('hello')) # N: Revealed type is "Any"
from myapp.models import ChildUser
reveal_type(ChildUser.objects) # N: Revealed type is "myapp.models.ChildUser_MyManager2[myapp.models.ChildUser]"
reveal_type(ChildUser.objects.select_related()) # N: Revealed type is "myapp.models.ChildUser_MyManager2[myapp.models.ChildUser]"
reveal_type(ChildUser.objects.select_related()) # N: Revealed type is "django.db.models.query._QuerySet[myapp.models.ChildUser*, myapp.models.ChildUser*]"
reveal_type(ChildUser.objects.get()) # N: Revealed type is "myapp.models.ChildUser*"
reveal_type(ChildUser.objects.get_instance()) # N: Revealed type is "builtins.int"
reveal_type(ChildUser.objects.get_instance_untyped('hello')) # N: Revealed type is "Any"

View File

@@ -50,6 +50,6 @@
...
out: |
main:7: error: Incompatible types in assignment (expression has type "Type[MyModel]", base class "SingleObjectMixin" defined the type as "Type[Other]")
main:8: error: Incompatible types in assignment (expression has type "Manager[MyModel]", base class "SingleObjectMixin" defined the type as "QuerySet[Other]")
main:10: error: Return type "QuerySet[MyModel]" of "get_queryset" incompatible with return type "QuerySet[Other]" in supertype "SingleObjectMixin"
main:12: error: Incompatible return value type (got "QuerySet[Other]", expected "QuerySet[MyModel]")
main:8: error: Incompatible types in assignment (expression has type "_QuerySet[MyModel, MyModel]", base class "SingleObjectMixin" defined the type as "_QuerySet[Other, Other]")
main:10: error: Return type "_QuerySet[MyModel, MyModel]" of "get_queryset" incompatible with return type "_QuerySet[Other, Other]" in supertype "SingleObjectMixin"
main:12: error: Incompatible return value type (got "_QuerySet[Other, Other]", expected "_QuerySet[MyModel, MyModel]")

View File

@@ -48,5 +48,5 @@
...
out: |
main:7: error: Incompatible types in assignment (expression has type "Type[MyModel]", base class "MultipleObjectMixin" defined the type as "Optional[Type[Other]]")
main:8: error: Incompatible types in assignment (expression has type "Manager[MyModel]", base class "MultipleObjectMixin" defined the type as "Optional[QuerySet[Other]]")
main:10: error: Return type "QuerySet[MyModel]" of "get_queryset" incompatible with return type "QuerySet[Other]" in supertype "MultipleObjectMixin"
main:8: error: Incompatible types in assignment (expression has type "_QuerySet[MyModel, MyModel]", base class "MultipleObjectMixin" defined the type as "Optional[_QuerySet[Other, Other]]")
main:10: error: Return type "_QuerySet[MyModel, MyModel]" of "get_queryset" incompatible with return type "_QuerySet[Other, Other]" in supertype "MultipleObjectMixin"