mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-14 15:57:08 +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
|
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
|
## 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
|
from .utils.version import get_version as get_version
|
||||||
|
|
||||||
@@ -7,7 +7,7 @@ __version__: str
|
|||||||
|
|
||||||
def setup(set_prefix: bool = ...) -> None: ...
|
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
|
# Used internally by mypy_django_plugin.
|
||||||
class _NamedTupleAnyAttr(NamedTuple):
|
class _AnyAttrAllowed(Protocol):
|
||||||
def __getattr__(self, item: str) -> Any: ...
|
def __getattr__(self, item: str) -> Any: ...
|
||||||
def __setattr__(self, item: str, value: Any) -> None: ...
|
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.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)
|
_T = TypeVar("_T", bound=Model, covariant=True)
|
||||||
_M = TypeVar("_M", bound="BaseManager")
|
_M = TypeVar("_M", bound="BaseManager")
|
||||||
|
|
||||||
class BaseManager(QuerySet[_T]):
|
class BaseManager(Generic[_T]):
|
||||||
creation_counter: int = ...
|
creation_counter: int = ...
|
||||||
auto_created: bool = ...
|
auto_created: bool = ...
|
||||||
use_in_migrations: 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 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 db_manager(self: _M, using: Optional[str] = ..., hints: Optional[Dict[str, Model]] = ...) -> _M: ...
|
||||||
def get_queryset(self) -> QuerySet[_T]: ...
|
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]): ...
|
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
|
from django.db.models.sql.query import Query, RawQuery
|
||||||
|
|
||||||
_T = TypeVar("_T", bound=models.Model, covariant=True)
|
_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]
|
model: Type[_T]
|
||||||
query: Query
|
query: Query
|
||||||
def __init__(
|
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 __class_getitem__(cls: Type[_QS], item: Type[_T]) -> Type[_QS]: ...
|
||||||
def __getstate__(self) -> Dict[str, Any]: ...
|
def __getstate__(self) -> Dict[str, Any]: ...
|
||||||
# Technically, the other QuerySet must be of the same type _T, but _T is covariant
|
# Technically, the other QuerySet must be of the same type _T, but _T is covariant
|
||||||
def __and__(self: _QS, other: QuerySet[_T]) -> _QS: ...
|
def __and__(self: _QS, other: _QuerySet[_T, _Row]) -> _QS: ...
|
||||||
def __or__(self: _QS, other: QuerySet[_T]) -> _QS: ...
|
def __or__(self: _QS, other: _QuerySet[_T, _Row]) -> _QS: ...
|
||||||
def iterator(self, chunk_size: int = ...) -> Iterator[_T]: ...
|
# 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 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 create(self, *args: Any, **kwargs: Any) -> _T: ...
|
||||||
def bulk_create(
|
def bulk_create(
|
||||||
self, objs: Iterable[_T], batch_size: Optional[int] = ..., ignore_conflicts: bool = ...
|
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(
|
def update_or_create(
|
||||||
self, defaults: Optional[MutableMapping[str, Any]] = ..., **kwargs: Any
|
self, defaults: Optional[MutableMapping[str, Any]] = ..., **kwargs: Any
|
||||||
) -> Tuple[_T, bool]: ...
|
) -> Tuple[_T, bool]: ...
|
||||||
def earliest(self, *fields: Any, field_name: Optional[Any] = ...) -> _T: ...
|
def earliest(self, *fields: Any, field_name: Optional[Any] = ...) -> _Row: ...
|
||||||
def latest(self, *fields: Any, field_name: Optional[Any] = ...) -> _T: ...
|
def latest(self, *fields: Any, field_name: Optional[Any] = ...) -> _Row: ...
|
||||||
def first(self) -> Optional[_T]: ...
|
def first(self) -> Optional[_Row]: ...
|
||||||
def last(self) -> Optional[_T]: ...
|
def last(self) -> Optional[_Row]: ...
|
||||||
def in_bulk(self, id_list: Iterable[Any] = ..., *, field_name: str = ...) -> Dict[Any, _T]: ...
|
def in_bulk(self, id_list: Iterable[Any] = ..., *, field_name: str = ...) -> Dict[Any, _T]: ...
|
||||||
def delete(self) -> Tuple[int, Dict[str, int]]: ...
|
def delete(self) -> Tuple[int, Dict[str, int]]: ...
|
||||||
def update(self, **kwargs: Any) -> int: ...
|
def update(self, **kwargs: Any) -> int: ...
|
||||||
@@ -78,15 +81,15 @@ class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
|
|||||||
using: Optional[str] = ...,
|
using: Optional[str] = ...,
|
||||||
) -> RawQuerySet: ...
|
) -> RawQuerySet: ...
|
||||||
# The type of values may be overridden to be more specific in the mypy plugin, depending on the fields param
|
# 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
|
# The type of values_list may be overridden to be more specific in the mypy plugin, depending on the fields param
|
||||||
def values_list(
|
def values_list(
|
||||||
self, *fields: Union[str, Combinable], flat: bool = ..., named: bool = ...
|
self, *fields: Union[str, Combinable], flat: bool = ..., named: bool = ...
|
||||||
) -> _ValuesQuerySet[_T, Any]: ...
|
) -> _QuerySet[_T, Any]: ...
|
||||||
def dates(self, field_name: str, kind: str, order: str = ...) -> _ValuesQuerySet[_T, datetime.date]: ...
|
def dates(self, field_name: str, kind: str, order: str = ...) -> _QuerySet[_T, datetime.date]: ...
|
||||||
def datetimes(
|
def datetimes(
|
||||||
self, field_name: str, kind: str, order: str = ..., tzinfo: Optional[datetime.tzinfo] = ...
|
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 none(self: _QS) -> _QS: ...
|
||||||
def all(self: _QS) -> _QS: ...
|
def all(self: _QS) -> _QS: ...
|
||||||
def filter(self: _QS, *args: Any, **kwargs: Any) -> _QS: ...
|
def filter(self: _QS, *args: Any, **kwargs: Any) -> _QS: ...
|
||||||
@@ -101,8 +104,7 @@ class QuerySet(Generic[_T], Collection[_T], Reversible[_T], Sized):
|
|||||||
) -> _QS: ...
|
) -> _QS: ...
|
||||||
def select_related(self: _QS, *fields: Any) -> _QS: ...
|
def select_related(self: _QS, *fields: Any) -> _QS: ...
|
||||||
def prefetch_related(self: _QS, *lookups: Any) -> _QS: ...
|
def prefetch_related(self: _QS, *lookups: Any) -> _QS: ...
|
||||||
# TODO: return type
|
def annotate(self: _QS, *args: Any, **kwargs: Any) -> _QS: ...
|
||||||
def annotate(self, *args: Any, **kwargs: Any) -> QuerySet[Any]: ...
|
|
||||||
def alias(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 order_by(self: _QS, *field_names: Any) -> _QS: ...
|
||||||
def distinct(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]] = ...,
|
tables: Optional[List[str]] = ...,
|
||||||
order_by: Optional[Sequence[str]] = ...,
|
order_by: Optional[Sequence[str]] = ...,
|
||||||
select_params: Optional[Sequence[Any]] = ...,
|
select_params: Optional[Sequence[Any]] = ...,
|
||||||
) -> QuerySet[Any]: ...
|
) -> _QuerySet[Any, Any]: ...
|
||||||
def reverse(self: _QS) -> _QS: ...
|
def reverse(self: _QS) -> _QS: ...
|
||||||
def defer(self: _QS, *fields: Any) -> _QS: ...
|
def defer(self: _QS, *fields: Any) -> _QS: ...
|
||||||
def only(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
|
@property
|
||||||
def db(self) -> str: ...
|
def db(self) -> str: ...
|
||||||
def resolve_expression(self, *args: Any, **kwargs: Any) -> Any: ...
|
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 __iter__(self) -> Iterator[_Row]: ...
|
||||||
|
def __contains__(self, x: object) -> bool: ...
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self, i: int) -> _Row: ...
|
def __getitem__(self, i: int) -> _Row: ...
|
||||||
@overload
|
@overload
|
||||||
def __getitem__(self: _QS, s: slice) -> _QS: ... # type: ignore
|
def __getitem__(self: _QS, s: slice) -> _QS: ...
|
||||||
def iterator(self, chunk_size: int = ...) -> Iterator[_Row]: ...
|
def __reversed__(self) -> 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]: ...
|
|
||||||
|
|
||||||
class RawQuerySet(Iterable[_T], Sized):
|
class RawQuerySet(Iterable[_T], Sized):
|
||||||
query: RawQuery
|
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 resolve_model_init_order(self) -> Tuple[List[str], List[int], List[Tuple[str, int]]]: ...
|
||||||
def using(self, alias: Optional[str]) -> RawQuerySet[_T]: ...
|
def using(self, alias: Optional[str]) -> RawQuerySet[_T]: ...
|
||||||
|
|
||||||
|
QuerySet = _QuerySet[_T, _T]
|
||||||
|
|
||||||
class Prefetch(object):
|
class Prefetch(object):
|
||||||
def __init__(self, lookup: str, queryset: Optional[QuerySet] = ..., to_attr: Optional[str] = ...) -> None: ...
|
def __init__(self, lookup: str, queryset: Optional[QuerySet] = ..., to_attr: Optional[str] = ...) -> None: ...
|
||||||
def __getstate__(self) -> Dict[str, Any]: ...
|
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.http import HttpRequest, HttpResponse
|
||||||
from django.views.generic.base import ContextMixin, TemplateResponseMixin, View
|
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):
|
class MultipleObjectMixin(Generic[T], ContextMixin):
|
||||||
allow_empty: bool = ...
|
allow_empty: bool = ...
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
from .aliases import ValuesQuerySet as ValuesQuerySet
|
from .aliases import ValuesQuerySet as ValuesQuerySet
|
||||||
|
from .annotations import Annotations as Annotations
|
||||||
|
from .annotations import WithAnnotations as WithAnnotations
|
||||||
from .patch import monkeypatch as monkeypatch
|
from .patch import monkeypatch as monkeypatch
|
||||||
|
|
||||||
__all__ = ["monkeypatch", "ValuesQuerySet"]
|
__all__ = ["monkeypatch", "ValuesQuerySet", "WithAnnotations", "Annotations"]
|
||||||
|
|||||||
@@ -1,8 +1,10 @@
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
if typing.TYPE_CHECKING:
|
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:
|
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.types import TypeOfAny, UnionType
|
||||||
|
|
||||||
from mypy_django_plugin.lib import fullnames, helpers
|
from mypy_django_plugin.lib import fullnames, helpers
|
||||||
|
from mypy_django_plugin.lib.fullnames import WITH_ANNOTATIONS_FULLNAME
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from django.contrib.postgres.fields import ArrayField
|
from django.contrib.postgres.fields import ArrayField
|
||||||
@@ -113,7 +114,15 @@ class DjangoContext:
|
|||||||
return modules
|
return modules
|
||||||
|
|
||||||
def get_model_class_by_fullname(self, fullname: str) -> Optional[Type[Model]]:
|
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(".")
|
module, _, model_cls_name = fullname.rpartition(".")
|
||||||
for model_cls in self.model_modules.get(module, set()):
|
for model_cls in self.model_modules.get(module, set()):
|
||||||
if model_cls.__name__ == model_cls_name:
|
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"
|
MANYTOMANY_FIELD_FULLNAME = "django.db.models.fields.related.ManyToManyField"
|
||||||
DUMMY_SETTINGS_BASE_CLASS = "django.conf._DjangoConfLazyObject"
|
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"
|
BASE_MANAGER_CLASS_FULLNAME = "django.db.models.manager.BaseManager"
|
||||||
MANAGER_CLASS_FULLNAME = "django.db.models.manager.Manager"
|
MANAGER_CLASS_FULLNAME = "django.db.models.manager.Manager"
|
||||||
RELATED_MANAGER_CLASS = "django.db.models.manager.RelatedManager"
|
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"
|
BASEFORM_CLASS_FULLNAME = "django.forms.forms.BaseForm"
|
||||||
FORM_CLASS_FULLNAME = "django.forms.forms.Form"
|
FORM_CLASS_FULLNAME = "django.forms.forms.Form"
|
||||||
MODELFORM_CLASS_FULLNAME = "django.forms.models.ModelForm"
|
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"
|
HTTPREQUEST_CLASS_FULLNAME = "django.http.request.HttpRequest"
|
||||||
|
|
||||||
F_EXPRESSION_FULLNAME = "django.db.models.expressions.F"
|
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.types import TypedDictType, TypeOfAny, UnionType
|
||||||
|
|
||||||
from mypy_django_plugin.lib import fullnames
|
from mypy_django_plugin.lib import fullnames
|
||||||
|
from mypy_django_plugin.lib.fullnames import WITH_ANNOTATIONS_FULLNAME
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from mypy_django_plugin.django.context import DjangoContext
|
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]:
|
def lookup_fully_qualified_sym(fullname: str, all_modules: Dict[str, MypyFile]) -> Optional[SymbolTableNode]:
|
||||||
if "." not in fullname:
|
if "." not in fullname:
|
||||||
return None
|
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, cls_name = fullname.rsplit(".", 1)
|
||||||
|
|
||||||
module_file = all_modules.get(module)
|
module_file = all_modules.get(module)
|
||||||
@@ -195,6 +204,10 @@ def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]
|
|||||||
return None
|
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(
|
def add_new_class_for_module(
|
||||||
module: MypyFile, name: str, bases: List[Instance], fields: Optional[Dict[str, MypyType]] = None
|
module: MypyFile, name: str, bases: List[Instance], fields: Optional[Dict[str, MypyType]] = None
|
||||||
) -> TypeInfo:
|
) -> TypeInfo:
|
||||||
@@ -233,10 +246,14 @@ def get_current_module(api: TypeChecker) -> MypyFile:
|
|||||||
return current_module
|
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)
|
current_module = get_current_module(api)
|
||||||
|
if extra_bases is None:
|
||||||
|
extra_bases = []
|
||||||
namedtuple_info = add_new_class_for_module(
|
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, []))
|
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(
|
for arg_name, arg_type, original_argument in zip(
|
||||||
method_type.arg_names[1:], method_type.arg_types[1:], original_arguments
|
method_type.arg_names[1:], method_type.arg_types[1:], original_arguments
|
||||||
):
|
):
|
||||||
bound_arg_type = semanal_api.anal_type(arg_type, allow_placeholder=True)
|
bound_arg_type = semanal_api.anal_type(arg_type)
|
||||||
if bound_arg_type is None and not semanal_api.final_iteration:
|
if bound_arg_type is None:
|
||||||
semanal_api.defer()
|
|
||||||
return
|
|
||||||
|
|
||||||
assert bound_arg_type is not None
|
|
||||||
|
|
||||||
if isinstance(bound_arg_type, PlaceholderNode):
|
|
||||||
return
|
return
|
||||||
|
|
||||||
var = Var(name=original_argument.variable.name, type=arg_type)
|
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.nodes import MypyFile, TypeInfo
|
||||||
from mypy.options import Options
|
from mypy.options import Options
|
||||||
from mypy.plugin import (
|
from mypy.plugin import (
|
||||||
|
AnalyzeTypeContext,
|
||||||
AttributeContext,
|
AttributeContext,
|
||||||
ClassDefContext,
|
ClassDefContext,
|
||||||
DynamicClassDefContext,
|
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.lib import fullnames, helpers
|
||||||
from mypy_django_plugin.transformers import fields, forms, init_create, meta, querysets, request, settings
|
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.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:
|
def transform_model_class(ctx: ClassDefContext, django_context: DjangoContext) -> None:
|
||||||
@@ -230,7 +235,7 @@ class NewSemanalDjangoPlugin(Plugin):
|
|||||||
related_model_module = related_model_cls.__module__
|
related_model_module = related_model_cls.__module__
|
||||||
if related_model_module != file.fullname:
|
if related_model_module != file.fullname:
|
||||||
deps.add(self._new_dependency(related_model_module))
|
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]]:
|
def get_function_hook(self, fullname: str) -> Optional[Callable[[FunctionContext], MypyType]]:
|
||||||
if fullname == "django.contrib.auth.get_user_model":
|
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):
|
if info and info.has_base(fullnames.FORM_MIXIN_CLASS_FULLNAME):
|
||||||
return forms.extract_proper_type_for_get_form
|
return forms.extract_proper_type_for_get_form
|
||||||
|
|
||||||
|
manager_classes = self._get_current_manager_bases()
|
||||||
|
|
||||||
if method_name == "values":
|
if method_name == "values":
|
||||||
info = self._get_typeinfo_or_none(class_fullname)
|
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)
|
return partial(querysets.extract_proper_type_queryset_values, django_context=self.django_context)
|
||||||
|
|
||||||
if method_name == "values_list":
|
if method_name == "values_list":
|
||||||
info = self._get_typeinfo_or_none(class_fullname)
|
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)
|
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":
|
if method_name == "get_field":
|
||||||
info = self._get_typeinfo_or_none(class_fullname)
|
info = self._get_typeinfo_or_none(class_fullname)
|
||||||
if info and info.has_base(fullnames.OPTIONS_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)
|
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":
|
if class_fullname in manager_classes and method_name == "create":
|
||||||
return partial(init_create.redefine_and_typecheck_model_create, django_context=self.django_context)
|
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"}:
|
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 partial(set_auth_user_model_boolean_fields, django_context=self.django_context)
|
||||||
return None
|
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]]:
|
def get_dynamic_class_hook(self, fullname: str) -> Optional[Callable[[DynamicClassDefContext], None]]:
|
||||||
if fullname.endswith("from_queryset"):
|
if fullname.endswith("from_queryset"):
|
||||||
class_name, _, _ = fullname.rpartition(".")
|
class_name, _, _ = fullname.rpartition(".")
|
||||||
|
|||||||
@@ -19,6 +19,16 @@ def create_new_manager_class_from_from_queryset_method(ctx: DynamicClassDefConte
|
|||||||
return
|
return
|
||||||
|
|
||||||
assert isinstance(base_manager_info, TypeInfo)
|
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(
|
new_manager_info = semanal_api.basic_new_typeinfo(
|
||||||
ctx.name, basetype_or_fallback=Instance(base_manager_info, [AnyType(TypeOfAny.unannotated)]), line=ctx.call.line
|
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 = semanal_api.cur_mod_node
|
||||||
current_module.names[ctx.name] = SymbolTableNode(GDEF, new_manager_info, plugin_generated=True)
|
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)
|
sym = semanal_api.lookup_fully_qualified_or_none(derived_queryset_fullname)
|
||||||
assert sym is not None
|
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.base import Model
|
||||||
from django.db.models.fields import DateField, DateTimeField
|
from django.db.models.fields import DateField, DateTimeField
|
||||||
from django.db.models.fields.related import ForeignKey
|
from django.db.models.fields.related import ForeignKey
|
||||||
from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel
|
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.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.plugins import common
|
||||||
from mypy.semanal import SemanticAnalyzer
|
from mypy.semanal import SemanticAnalyzer
|
||||||
from mypy.types import AnyType, Instance
|
from mypy.types import AnyType, Instance
|
||||||
from mypy.types import Type as MypyType
|
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.django.context import DjangoContext
|
||||||
from mypy_django_plugin.lib import fullnames, helpers
|
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 import fields
|
||||||
from mypy_django_plugin.transformers.fields import get_field_descriptor_types
|
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():
|
for manager_name, manager in model_cls._meta.managers_map.items():
|
||||||
manager_class_name = manager.__class__.__name__
|
manager_class_name = manager.__class__.__name__
|
||||||
manager_fullname = helpers.get_class_fullname(manager.__class__)
|
manager_fullname = helpers.get_class_fullname(manager.__class__)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
manager_info = self.lookup_typeinfo_or_incomplete_defn_error(manager_fullname)
|
manager_info = self.lookup_typeinfo_or_incomplete_defn_error(manager_fullname)
|
||||||
except helpers.IncompleteDefnException as exc:
|
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)
|
boolinfo = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), bool)
|
||||||
assert boolinfo is not None
|
assert boolinfo is not None
|
||||||
return Instance(boolinfo, [])
|
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.django.context import DjangoContext
|
||||||
from mypy_django_plugin.lib import fullnames, helpers
|
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:
|
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)
|
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)
|
lookup_type = django_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg)
|
||||||
# Managers as provided_type is not supported yet
|
# Managers as provided_type is not supported yet
|
||||||
if isinstance(provided_type, Instance) and helpers.has_any_of_bases(
|
if isinstance(provided_type, Instance) and helpers.has_any_of_bases(
|
||||||
|
|||||||
@@ -1,18 +1,21 @@
|
|||||||
from collections import OrderedDict
|
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.core.exceptions import FieldError
|
||||||
from django.db.models.base import Model
|
from django.db.models.base import Model
|
||||||
from django.db.models.fields.related import RelatedField
|
from django.db.models.fields.related import RelatedField
|
||||||
from django.db.models.fields.reverse_related import ForeignObjectRel
|
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.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 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.django.context import DjangoContext, LookupsAreUnsupported
|
||||||
from mypy_django_plugin.lib import fullnames, helpers
|
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]:
|
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(
|
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]:
|
) -> Optional[MypyType]:
|
||||||
try:
|
try:
|
||||||
lookup_field = django_context.resolve_lookup_into_field(model_cls, lookup)
|
lookup_field = django_context.resolve_lookup_into_field(model_cls, lookup)
|
||||||
except FieldError as exc:
|
except FieldError as exc:
|
||||||
|
if not silent_on_error:
|
||||||
ctx.api.fail(exc.args[0], ctx.context)
|
ctx.api.fail(exc.args[0], ctx.context)
|
||||||
return None
|
return None
|
||||||
except LookupsAreUnsupported:
|
except LookupsAreUnsupported:
|
||||||
@@ -61,7 +71,13 @@ def get_field_type_from_lookup(
|
|||||||
|
|
||||||
|
|
||||||
def get_values_list_row_type(
|
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:
|
) -> MypyType:
|
||||||
field_lookups = resolve_field_lookups(ctx.args[0], django_context)
|
field_lookups = resolve_field_lookups(ctx.args[0], django_context)
|
||||||
if field_lookups is None:
|
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):
|
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_type = django_context.get_field_get_type(typechecker_api, field, method="values_list")
|
||||||
column_types[field.attname] = column_type
|
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)
|
return helpers.make_oneoff_named_tuple(typechecker_api, "Row", column_types)
|
||||||
else:
|
else:
|
||||||
# flat=False, named=False, all fields
|
# flat=False, named=False, all fields
|
||||||
|
if is_annotated:
|
||||||
|
return typechecker_api.named_generic_type("builtins.tuple", [AnyType(TypeOfAny.special_form)])
|
||||||
field_lookups = []
|
field_lookups = []
|
||||||
for field in django_context.get_model_fields(model_cls):
|
for field in django_context.get_model_fields(model_cls):
|
||||||
field_lookups.append(field.attname)
|
field_lookups.append(field.attname)
|
||||||
@@ -95,9 +122,12 @@ def get_values_list_row_type(
|
|||||||
column_types = OrderedDict()
|
column_types = OrderedDict()
|
||||||
for field_lookup in field_lookups:
|
for field_lookup in field_lookups:
|
||||||
lookup_field_type = get_field_type_from_lookup(
|
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 lookup_field_type is None:
|
||||||
|
if is_annotated:
|
||||||
|
lookup_field_type = AnyType(TypeOfAny.from_omitted_generics)
|
||||||
|
else:
|
||||||
return AnyType(TypeOfAny.from_error)
|
return AnyType(TypeOfAny.from_error)
|
||||||
column_types[field_lookup] = lookup_field_type
|
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:
|
def extract_proper_type_queryset_values_list(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
|
||||||
# called on the Instance, returns QuerySet of something
|
# called on the Instance, returns QuerySet of something
|
||||||
assert isinstance(ctx.type, Instance)
|
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)
|
model_type = _extract_model_type_from_queryset(ctx.type)
|
||||||
if model_type is None:
|
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)
|
model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname)
|
||||||
if model_cls is None:
|
if model_cls is None:
|
||||||
return ctx.default_return_type
|
return default_return_type
|
||||||
|
|
||||||
flat_expr = helpers.get_call_argument_by_name(ctx, "flat")
|
flat_expr = helpers.get_call_argument_by_name(ctx, "flat")
|
||||||
if flat_expr is not None and isinstance(flat_expr, NameExpr):
|
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:
|
if flat and named:
|
||||||
ctx.api.fail("'flat' and 'named' can't be used together", ctx.context)
|
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
|
# account for possible None
|
||||||
flat = flat or False
|
flat = flat or False
|
||||||
named = named or False
|
named = named or False
|
||||||
|
|
||||||
row_type = get_values_list_row_type(ctx, django_context, model_cls, flat=flat, named=named)
|
is_annotated = is_annotated_model_fullname(model_type.type.fullname)
|
||||||
return helpers.reparametrize_instance(ctx.default_return_type, [model_type, row_type])
|
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]]:
|
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:
|
def extract_proper_type_queryset_values(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
|
||||||
# called on QuerySet, return QuerySet of something
|
# called on QuerySet, return QuerySet of something
|
||||||
assert isinstance(ctx.type, Instance)
|
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)
|
model_type = _extract_model_type_from_queryset(ctx.type)
|
||||||
if model_type is None:
|
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)
|
model_cls = django_context.get_model_class_by_fullname(model_type.type.fullname)
|
||||||
if model_cls is None:
|
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)
|
field_lookups = resolve_field_lookups(ctx.args[0], django_context)
|
||||||
if field_lookups is None:
|
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"
|
ctx, django_context, model_cls, lookup=field_lookup, method="values"
|
||||||
)
|
)
|
||||||
if field_lookup_type is None:
|
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
|
column_types[field_lookup] = field_lookup_type
|
||||||
|
|
||||||
row_type = helpers.make_typeddict(ctx.api, column_types, set(column_types.keys()))
|
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
|
pass
|
||||||
|
|
||||||
class A(admin.ModelAdmin):
|
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
|
- case: errors_for_invalid_model_admin_generic
|
||||||
main: |
|
main: |
|
||||||
from django.contrib.admin import ModelAdmin
|
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
|
from myapp.models import Blog
|
||||||
|
|
||||||
qs = Blog.objects.all()
|
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(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(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.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.first()) # N: Revealed type is "Union[myapp.models.Blog*, None]"
|
||||||
reveal_type(qs.earliest()) # N: Revealed type is "myapp.models.Blog*"
|
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[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*]"
|
reveal_type(qs.in_bulk()) # N: Revealed type is "builtins.dict[Any, myapp.models.Blog*]"
|
||||||
|
|
||||||
# .dates / .datetimes
|
# .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.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._ValuesQuerySet[myapp.models.Blog*, datetime.datetime]"
|
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
|
# 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:
|
installed_apps:
|
||||||
- myapp
|
- myapp
|
||||||
files:
|
files:
|
||||||
|
|||||||
@@ -111,8 +111,8 @@
|
|||||||
- case: values_of_many_to_many_field
|
- case: values_of_many_to_many_field
|
||||||
main: |
|
main: |
|
||||||
from myapp.models import Author, Book
|
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(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._ValuesQuerySet[myapp.models.Author, TypedDict({'books': 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:
|
installed_apps:
|
||||||
- myapp
|
- myapp
|
||||||
files:
|
files:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
reveal_type(query.all().get()) # N: Revealed type is "Tuple[builtins.str]"
|
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.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.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:
|
installed_apps:
|
||||||
- myapp
|
- myapp
|
||||||
files:
|
files:
|
||||||
@@ -214,8 +214,8 @@
|
|||||||
- case: values_list_flat_true_with_ids
|
- case: values_list_flat_true_with_ids
|
||||||
main: |
|
main: |
|
||||||
from myapp.models import Blog, Publisher
|
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('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._ValuesQuerySet[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]
|
# is Iterable[int]
|
||||||
reveal_type(list(Blog.objects.values_list('id', flat=True))) # N: Revealed type is "builtins.list[builtins.int*]"
|
reveal_type(list(Blog.objects.values_list('id', flat=True))) # N: Revealed type is "builtins.list[builtins.int*]"
|
||||||
installed_apps:
|
installed_apps:
|
||||||
@@ -234,8 +234,8 @@
|
|||||||
main: |
|
main: |
|
||||||
from myapp.models import TransactionQuerySet
|
from myapp.models import TransactionQuerySet
|
||||||
reveal_type(TransactionQuerySet()) # N: Revealed type is "myapp.models.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()) # 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._ValuesQuerySet[myapp.models.Transaction, Tuple[builtins.int, 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:
|
installed_apps:
|
||||||
- myapp
|
- myapp
|
||||||
files:
|
files:
|
||||||
@@ -251,8 +251,8 @@
|
|||||||
- case: values_list_of_many_to_many_field
|
- case: values_list_of_many_to_many_field
|
||||||
main: |
|
main: |
|
||||||
from myapp.models import Author, Book
|
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(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._ValuesQuerySet[myapp.models.Author, 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:
|
installed_apps:
|
||||||
- myapp
|
- myapp
|
||||||
files:
|
files:
|
||||||
|
|||||||
@@ -308,14 +308,14 @@
|
|||||||
main: |
|
main: |
|
||||||
from myapp.models import User
|
from myapp.models import User
|
||||||
reveal_type(User.objects) # N: Revealed type is "myapp.models.User_MyManager2[myapp.models.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()) # 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()) # N: Revealed type is "builtins.int"
|
||||||
reveal_type(User.objects.get_instance_untyped('hello')) # N: Revealed type is "Any"
|
reveal_type(User.objects.get_instance_untyped('hello')) # N: Revealed type is "Any"
|
||||||
|
|
||||||
from myapp.models import ChildUser
|
from myapp.models import ChildUser
|
||||||
reveal_type(ChildUser.objects) # N: Revealed type is "myapp.models.ChildUser_MyManager2[myapp.models.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()) # 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()) # N: Revealed type is "builtins.int"
|
||||||
reveal_type(ChildUser.objects.get_instance_untyped('hello')) # N: Revealed type is "Any"
|
reveal_type(ChildUser.objects.get_instance_untyped('hello')) # N: Revealed type is "Any"
|
||||||
|
|||||||
@@ -50,6 +50,6 @@
|
|||||||
...
|
...
|
||||||
out: |
|
out: |
|
||||||
main:7: error: Incompatible types in assignment (expression has type "Type[MyModel]", base class "SingleObjectMixin" defined the type as "Type[Other]")
|
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: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]" of "get_queryset" incompatible with return type "QuerySet[Other]" in supertype "SingleObjectMixin"
|
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]", expected "QuerySet[MyModel]")
|
main:12: error: Incompatible return value type (got "_QuerySet[Other, Other]", expected "_QuerySet[MyModel, MyModel]")
|
||||||
|
|||||||
@@ -48,5 +48,5 @@
|
|||||||
...
|
...
|
||||||
out: |
|
out: |
|
||||||
main:7: error: Incompatible types in assignment (expression has type "Type[MyModel]", base class "MultipleObjectMixin" defined the type as "Optional[Type[Other]]")
|
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: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]" of "get_queryset" incompatible with return type "QuerySet[Other]" in supertype "MultipleObjectMixin"
|
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