mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-14 07:47:09 +08:00
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:
42
README.md
42
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
|
||||
|
||||
|
||||
@@ -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: ...
|
||||
|
||||
@@ -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]): ...
|
||||
|
||||
|
||||
@@ -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]: ...
|
||||
|
||||
@@ -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 = ...
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
22
django_stubs_ext/django_stubs_ext/annotations.py
Normal file
22
django_stubs_ext/django_stubs_ext/annotations.py
Normal 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]`.
|
||||
"""
|
||||
8
django_stubs_ext/tests/test_aliases.py
Normal file
8
django_stubs_ext/tests/test_aliases.py
Normal 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
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,6 +62,14 @@ 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
|
||||
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)
|
||||
@@ -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)
|
||||
|
||||
@@ -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(".")
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,6 +30,10 @@ def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext)
|
||||
):
|
||||
provided_type = resolve_combinable_type(provided_type, django_context)
|
||||
|
||||
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(
|
||||
|
||||
@@ -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,11 +41,18 @@ 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:
|
||||
if not silent_on_error:
|
||||
ctx.api.fail(exc.args[0], ctx.context)
|
||||
return None
|
||||
except LookupsAreUnsupported:
|
||||
@@ -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
|
||||
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,9 +122,12 @@ 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:
|
||||
if is_annotated:
|
||||
lookup_field_type = AnyType(TypeOfAny.from_omitted_generics)
|
||||
else:
|
||||
return AnyType(TypeOfAny.from_error)
|
||||
column_types[field_lookup] = lookup_field_type
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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
|
||||
|
||||
349
tests/typecheck/managers/querysets/test_annotate.yml
Normal file
349
tests/typecheck/managers/querysets/test_annotate.yml
Normal 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)
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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]")
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user