diff --git a/README.md b/README.md index 6505c1ce9..1b430f510 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/django-stubs/__init__.pyi b/django-stubs/__init__.pyi index c705a44c5..689c4f472 100644 --- a/django-stubs/__init__.pyi +++ b/django-stubs/__init__.pyi @@ -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: ... diff --git a/django-stubs/db/models/manager.pyi b/django-stubs/db/models/manager.pyi index 263530a27..497550286 100644 --- a/django-stubs/db/models/manager.pyi +++ b/django-stubs/db/models/manager.pyi @@ -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]): ... diff --git a/django-stubs/db/models/query.pyi b/django-stubs/db/models/query.pyi index 8395a0847..bd725c470 100644 --- a/django-stubs/db/models/query.pyi +++ b/django-stubs/db/models/query.pyi @@ -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]: ... diff --git a/django-stubs/views/generic/list.pyi b/django-stubs/views/generic/list.pyi index 671823240..b8c8d0204 100644 --- a/django-stubs/views/generic/list.pyi +++ b/django-stubs/views/generic/list.pyi @@ -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 = ... diff --git a/django_stubs_ext/django_stubs_ext/__init__.py b/django_stubs_ext/django_stubs_ext/__init__.py index 999fb49e6..92bc220b5 100644 --- a/django_stubs_ext/django_stubs_ext/__init__.py +++ b/django_stubs_ext/django_stubs_ext/__init__.py @@ -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"] diff --git a/django_stubs_ext/django_stubs_ext/aliases.py b/django_stubs_ext/django_stubs_ext/aliases.py index ccaa345a5..7ff4dbe5c 100644 --- a/django_stubs_ext/django_stubs_ext/aliases.py +++ b/django_stubs_ext/django_stubs_ext/aliases.py @@ -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 diff --git a/django_stubs_ext/django_stubs_ext/annotations.py b/django_stubs_ext/django_stubs_ext/annotations.py new file mode 100644 index 000000000..3c819af34 --- /dev/null +++ b/django_stubs_ext/django_stubs_ext/annotations.py @@ -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]`. +""" diff --git a/django_stubs_ext/tests/test_aliases.py b/django_stubs_ext/tests/test_aliases.py new file mode 100644 index 000000000..015dad4ea --- /dev/null +++ b/django_stubs_ext/tests/test_aliases.py @@ -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 diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index b9099c8a7..fcdab28c9 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -21,6 +21,7 @@ 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 @@ def model_modules(self) -> Dict[str, Set[Type[Model]]]: 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: diff --git a/mypy_django_plugin/lib/fullnames.py b/mypy_django_plugin/lib/fullnames.py index f2bca6743..1441a55fb 100644 --- a/mypy_django_plugin/lib/fullnames.py +++ b/mypy_django_plugin/lib/fullnames.py @@ -11,11 +11,14 @@ 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 @@ HTTPREQUEST_CLASS_FULLNAME = "django.http.request.HttpRequest" F_EXPRESSION_FULLNAME = "django.db.models.expressions.F" + +ANY_ATTR_ALLOWED_CLASS_FULLNAME = "django._AnyAttrAllowed" diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 64eeba7b0..198916d00 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -41,6 +41,7 @@ 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) diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index a00b5e1af..6baa7cf55 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -10,6 +10,7 @@ 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.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 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]: 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 @@ def get_method_hook(self, fullname: str) -> Optional[Callable[[MethodContext], M 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 @@ def get_attribute_hook(self, fullname: str) -> Optional[Callable[[AttributeConte 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(".") diff --git a/mypy_django_plugin/transformers/managers.py b/mypy_django_plugin/transformers/managers.py index 608477bf2..13bb39bc4 100644 --- a/mypy_django_plugin/transformers/managers.py +++ b/mypy_django_plugin/transformers/managers.py @@ -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 diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 4321ee858..305ade90a 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -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 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None: 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 diff --git a/mypy_django_plugin/transformers/orm_lookups.py b/mypy_django_plugin/transformers/orm_lookups.py index 91c55c99a..e3adc1d6b 100644 --- a/mypy_django_plugin/transformers/orm_lookups.py +++ b/mypy_django_plugin/transformers/orm_lookups.py @@ -5,6 +5,7 @@ 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) diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py index 69a6e30d0..566101908 100644 --- a/mypy_django_plugin/transformers/querysets.py +++ b/mypy_django_plugin/transformers/querysets.py @@ -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]) diff --git a/tests/typecheck/contrib/admin/test_options.yml b/tests/typecheck/contrib/admin/test_options.yml index 080755ca7..3d13652a6 100644 --- a/tests/typecheck/contrib/admin/test_options.yml +++ b/tests/typecheck/contrib/admin/test_options.yml @@ -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 diff --git a/tests/typecheck/managers/querysets/test_annotate.yml b/tests/typecheck/managers/querysets/test_annotate.yml new file mode 100644 index 000000000..af513b612 --- /dev/null +++ b/tests/typecheck/managers/querysets/test_annotate.yml @@ -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) diff --git a/tests/typecheck/managers/querysets/test_basic_methods.yml b/tests/typecheck/managers/querysets/test_basic_methods.yml index c540a7da9..aec7a537c 100644 --- a/tests/typecheck/managers/querysets/test_basic_methods.yml +++ b/tests/typecheck/managers/querysets/test_basic_methods.yml @@ -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: diff --git a/tests/typecheck/managers/querysets/test_filter.yml b/tests/typecheck/managers/querysets/test_filter.yml index 43056f55a..3418ac55f 100644 --- a/tests/typecheck/managers/querysets/test_filter.yml +++ b/tests/typecheck/managers/querysets/test_filter.yml @@ -253,4 +253,4 @@ class User(models.Model): username = models.TextField() username2 = models.TextField() - age = models.IntegerField() \ No newline at end of file + age = models.IntegerField() diff --git a/tests/typecheck/managers/querysets/test_values.yml b/tests/typecheck/managers/querysets/test_values.yml index 0ee4b069b..dc2e762fa 100644 --- a/tests/typecheck/managers/querysets/test_values.yml +++ b/tests/typecheck/managers/querysets/test_values.yml @@ -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: diff --git a/tests/typecheck/managers/querysets/test_values_list.yml b/tests/typecheck/managers/querysets/test_values_list.yml index db0eed19e..18f16603b 100644 --- a/tests/typecheck/managers/querysets/test_values_list.yml +++ b/tests/typecheck/managers/querysets/test_values_list.yml @@ -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: diff --git a/tests/typecheck/managers/test_managers.yml b/tests/typecheck/managers/test_managers.yml index 1548afe45..f1129a0f6 100644 --- a/tests/typecheck/managers/test_managers.yml +++ b/tests/typecheck/managers/test_managers.yml @@ -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" diff --git a/tests/typecheck/views/generic/test_detail.yml b/tests/typecheck/views/generic/test_detail.yml index 8d65e4a4c..b5df0829e 100644 --- a/tests/typecheck/views/generic/test_detail.yml +++ b/tests/typecheck/views/generic/test_detail.yml @@ -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]") diff --git a/tests/typecheck/views/generic/test_list.yml b/tests/typecheck/views/generic/test_list.yml index b32afef49..2ef0301c7 100644 --- a/tests/typecheck/views/generic/test_list.yml +++ b/tests/typecheck/views/generic/test_list.yml @@ -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"