mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-07 04:34:29 +08:00
add support for typechecking of filter/get/exclude arguments (#183)
* add support for typechecking of filter/get/exclude arguments * linting
This commit is contained in:
@@ -43,6 +43,7 @@ _GT = TypeVar("_GT")
|
||||
class Field(RegisterLookupMixin, Generic[_ST, _GT]):
|
||||
_pyi_private_set_type: Any
|
||||
_pyi_private_get_type: Any
|
||||
_pyi_lookup_exact_type: Any
|
||||
|
||||
widget: Widget
|
||||
help_text: str
|
||||
@@ -131,6 +132,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
|
||||
class IntegerField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[float, int, str, Combinable]
|
||||
_pyi_private_get_type: int
|
||||
_pyi_lookup_exact_type: int
|
||||
|
||||
class PositiveIntegerRelDbTypeMixin:
|
||||
def rel_db_type(self, connection: Any): ...
|
||||
@@ -143,10 +145,12 @@ class BigIntegerField(IntegerField[_ST, _GT]): ...
|
||||
class FloatField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[float, int, str, Combinable]
|
||||
_pyi_private_get_type: float
|
||||
_pyi_lookup_exact_type: float
|
||||
|
||||
class DecimalField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[str, float, decimal.Decimal, Combinable]
|
||||
_pyi_private_get_type: decimal.Decimal
|
||||
_pyi_lookup_exact_type: Union[str, decimal.Decimal]
|
||||
# attributes
|
||||
max_digits: int = ...
|
||||
decimal_places: int = ...
|
||||
@@ -176,10 +180,13 @@ class DecimalField(Field[_ST, _GT]):
|
||||
class AutoField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[Combinable, int, str]
|
||||
_pyi_private_get_type: int
|
||||
_pyi_lookup_exact_type: int
|
||||
|
||||
class CharField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[str, int, Combinable]
|
||||
_pyi_private_get_type: str
|
||||
# objects are converted to string before comparison
|
||||
_pyi_lookup_exact_type: Any
|
||||
def __init__(
|
||||
self,
|
||||
verbose_name: Optional[Union[str, bytes]] = ...,
|
||||
@@ -238,14 +245,18 @@ class URLField(CharField[_ST, _GT]): ...
|
||||
class TextField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[str, Combinable]
|
||||
_pyi_private_get_type: str
|
||||
# objects are converted to string before comparison
|
||||
_pyi_lookup_exact_type: Any
|
||||
|
||||
class BooleanField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[bool, Combinable]
|
||||
_pyi_private_get_type: bool
|
||||
_pyi_lookup_exact_type: bool
|
||||
|
||||
class NullBooleanField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Optional[Union[bool, Combinable]]
|
||||
_pyi_private_get_type: Optional[bool]
|
||||
_pyi_lookup_exact_type: Optional[bool]
|
||||
|
||||
class IPAddressField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[str, Combinable]
|
||||
@@ -286,6 +297,7 @@ class DateTimeCheckMixin: ...
|
||||
class DateField(DateTimeCheckMixin, Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[str, date, Combinable]
|
||||
_pyi_private_get_type: date
|
||||
_pyi_lookup_exact_type: Union[str, date]
|
||||
def __init__(
|
||||
self,
|
||||
verbose_name: Optional[Union[str, bytes]] = ...,
|
||||
@@ -338,6 +350,7 @@ class TimeField(DateTimeCheckMixin, Field[_ST, _GT]):
|
||||
|
||||
class DateTimeField(DateField[_ST, _GT]):
|
||||
_pyi_private_get_type: datetime
|
||||
_pyi_lookup_exact_type: Union[str, datetime]
|
||||
|
||||
class UUIDField(Field[_ST, _GT]):
|
||||
_pyi_private_set_type: Union[str, uuid.UUID]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from datetime import datetime
|
||||
from typing import Any, Iterable, List, Mapping, Optional, Tuple, Type, Union
|
||||
from typing import Any, Iterable, List, Optional, Tuple, Type, Union, Mapping, TypeVar, Generic
|
||||
|
||||
from django.db.backends.sqlite3.base import DatabaseWrapper
|
||||
from django.db.models.expressions import Expression, Func
|
||||
@@ -10,7 +10,9 @@ from django.utils.safestring import SafeText
|
||||
|
||||
from django.db.models.fields import TextField, related_lookups
|
||||
|
||||
class Lookup:
|
||||
_T = TypeVar("_T")
|
||||
|
||||
class Lookup(Generic[_T]):
|
||||
lookup_name: str = ...
|
||||
prepare_rhs: bool = ...
|
||||
can_use_none_as_rhs: bool = ...
|
||||
@@ -47,7 +49,7 @@ class Transform(RegisterLookupMixin, Func):
|
||||
def lhs(self) -> Expression: ...
|
||||
def get_bilateral_transforms(self) -> List[Type[Transform]]: ...
|
||||
|
||||
class BuiltinLookup(Lookup):
|
||||
class BuiltinLookup(Lookup[_T]):
|
||||
def get_rhs_op(self, connection: DatabaseWrapper, rhs: str) -> str: ...
|
||||
|
||||
class FieldGetDbPrepValueMixin:
|
||||
@@ -62,21 +64,21 @@ class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin):
|
||||
class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): ...
|
||||
class IExact(BuiltinLookup): ...
|
||||
class GreaterThan(FieldGetDbPrepValueMixin, BuiltinLookup): ...
|
||||
class GreaterThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): ...
|
||||
class LessThan(FieldGetDbPrepValueMixin, BuiltinLookup): ...
|
||||
class GreaterThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup[_T]): ...
|
||||
class LessThan(FieldGetDbPrepValueMixin, BuiltinLookup[_T]): ...
|
||||
class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): ...
|
||||
|
||||
class IntegerFieldFloatRounding:
|
||||
rhs: Any = ...
|
||||
def get_prep_lookup(self) -> Any: ...
|
||||
|
||||
class IntegerGreaterThanOrEqual(IntegerFieldFloatRounding, GreaterThanOrEqual): ...
|
||||
class IntegerLessThan(IntegerFieldFloatRounding, LessThan): ...
|
||||
class IntegerGreaterThanOrEqual(IntegerFieldFloatRounding, GreaterThanOrEqual[Union[int, float]]): ...
|
||||
class IntegerLessThan(IntegerFieldFloatRounding, LessThan[Union[int, float]]): ...
|
||||
|
||||
class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
|
||||
def split_parameter_list_as_sql(self, compiler: Any, connection: Any): ...
|
||||
|
||||
class PatternLookup(BuiltinLookup):
|
||||
class PatternLookup(BuiltinLookup[str]):
|
||||
param_pattern: str = ...
|
||||
|
||||
class Contains(PatternLookup): ...
|
||||
@@ -86,8 +88,8 @@ class IStartsWith(StartsWith): ...
|
||||
class EndsWith(PatternLookup): ...
|
||||
class IEndsWith(EndsWith): ...
|
||||
class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup): ...
|
||||
class IsNull(BuiltinLookup): ...
|
||||
class Regex(BuiltinLookup): ...
|
||||
class IsNull(BuiltinLookup[bool]): ...
|
||||
class Regex(BuiltinLookup[str]): ...
|
||||
class IRegex(Regex): ...
|
||||
|
||||
class YearLookup(Lookup):
|
||||
|
||||
@@ -2,7 +2,7 @@ import os
|
||||
from collections import defaultdict
|
||||
from contextlib import contextmanager
|
||||
from typing import (
|
||||
TYPE_CHECKING, Dict, Iterator, Optional, Set, Tuple, Type, Union,
|
||||
TYPE_CHECKING, Dict, Iterable, Iterator, Optional, Set, Tuple, Type, Union,
|
||||
)
|
||||
|
||||
from django.core.exceptions import FieldError
|
||||
@@ -11,14 +11,16 @@ from django.db.models.base import Model
|
||||
from django.db.models.fields import AutoField, CharField, Field
|
||||
from django.db.models.fields.related import ForeignKey, RelatedField
|
||||
from django.db.models.fields.reverse_related import ForeignObjectRel
|
||||
from django.db.models.lookups import Exact
|
||||
from django.db.models.sql.query import Query
|
||||
from django.utils.functional import cached_property
|
||||
from mypy.checker import TypeChecker
|
||||
from mypy.plugin import MethodContext
|
||||
from mypy.types import AnyType, Instance
|
||||
from mypy.types import Type as MypyType
|
||||
from mypy.types import TypeOfAny
|
||||
from mypy.types import TypeOfAny, UnionType
|
||||
|
||||
from mypy_django_plugin.lib import helpers
|
||||
from mypy_django_plugin.lib import fullnames, helpers
|
||||
|
||||
try:
|
||||
from django.contrib.postgres.fields import ArrayField
|
||||
@@ -153,33 +155,87 @@ class DjangoFieldsContext:
|
||||
return related_model_cls
|
||||
|
||||
|
||||
class LookupsAreUnsupported(Exception):
|
||||
pass
|
||||
|
||||
|
||||
class DjangoLookupsContext:
|
||||
def __init__(self, django_context: 'DjangoContext'):
|
||||
self.django_context = django_context
|
||||
|
||||
def resolve_lookup(self, model_cls: Type[Model], lookup: str) -> Field:
|
||||
def _resolve_field_from_parts(self, field_parts: Iterable[str], model_cls: Type[Model]) -> Field:
|
||||
currently_observed_model = model_cls
|
||||
field = None
|
||||
for field_part in field_parts:
|
||||
if field_part == 'pk':
|
||||
field = self.django_context.get_primary_key_field(currently_observed_model)
|
||||
continue
|
||||
|
||||
field = currently_observed_model._meta.get_field(field_part)
|
||||
if isinstance(field, RelatedField):
|
||||
currently_observed_model = field.related_model
|
||||
model_name = currently_observed_model._meta.model_name
|
||||
if (model_name is not None
|
||||
and field_part == (model_name + '_id')):
|
||||
field = self.django_context.get_primary_key_field(currently_observed_model)
|
||||
|
||||
if isinstance(field, ForeignObjectRel):
|
||||
currently_observed_model = field.related_model
|
||||
|
||||
assert field is not None
|
||||
return field
|
||||
|
||||
def resolve_lookup_info_field(self, model_cls: Type[Model], lookup: str) -> Field:
|
||||
query = Query(model_cls)
|
||||
lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)
|
||||
if lookup_parts:
|
||||
raise FieldError('Lookups not supported yet')
|
||||
raise LookupsAreUnsupported()
|
||||
|
||||
currently_observed_model = model_cls
|
||||
current_field = None
|
||||
for field_part in field_parts:
|
||||
if field_part == 'pk':
|
||||
return self.django_context.get_primary_key_field(currently_observed_model)
|
||||
return self._resolve_field_from_parts(field_parts, model_cls)
|
||||
|
||||
current_field = currently_observed_model._meta.get_field(field_part)
|
||||
if not isinstance(current_field, (ForeignObjectRel, RelatedField)):
|
||||
continue
|
||||
def resolve_lookup_expected_type(self, ctx: MethodContext, model_cls: Type[Model], lookup: str) -> MypyType:
|
||||
query = Query(model_cls)
|
||||
try:
|
||||
lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)
|
||||
if is_expression:
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
except FieldError as exc:
|
||||
ctx.api.fail(exc.args[0], ctx.context)
|
||||
return AnyType(TypeOfAny.from_error)
|
||||
|
||||
currently_observed_model = self.django_context.fields_context.get_related_model_cls(current_field)
|
||||
if isinstance(current_field, ForeignObjectRel):
|
||||
current_field = self.django_context.get_primary_key_field(currently_observed_model)
|
||||
field = self._resolve_field_from_parts(field_parts, model_cls)
|
||||
|
||||
# if it is None, solve_lookup_type() will fail earlier
|
||||
assert current_field is not None
|
||||
return current_field
|
||||
lookup_cls = None
|
||||
if lookup_parts:
|
||||
lookup = lookup_parts[-1]
|
||||
lookup_cls = field.get_lookup(lookup)
|
||||
if lookup_cls is None:
|
||||
# unknown lookup
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
|
||||
if lookup_cls is None or isinstance(lookup_cls, Exact):
|
||||
return self.django_context.get_field_lookup_exact_type(helpers.get_typechecker_api(ctx), field)
|
||||
|
||||
assert lookup_cls is not None
|
||||
|
||||
lookup_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), lookup_cls)
|
||||
if lookup_info is None:
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
|
||||
for lookup_base in helpers.iter_bases(lookup_info):
|
||||
if lookup_base.args and isinstance(lookup_base.args[0], Instance):
|
||||
lookup_type: MypyType = lookup_base.args[0]
|
||||
# if it's Field, consider lookup_type a __get__ of current field
|
||||
if (isinstance(lookup_type, Instance)
|
||||
and lookup_type.type.fullname() == fullnames.FIELD_FULLNAME):
|
||||
field_info = helpers.lookup_class_typeinfo(helpers.get_typechecker_api(ctx), field.__class__)
|
||||
if field_info is None:
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
lookup_type = helpers.get_private_descriptor_type(field_info, '_pyi_private_get_type',
|
||||
is_nullable=field.null)
|
||||
return lookup_type
|
||||
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
|
||||
|
||||
class DjangoContext:
|
||||
@@ -228,6 +284,27 @@ class DjangoContext:
|
||||
if isinstance(field, ForeignObjectRel):
|
||||
yield field
|
||||
|
||||
def get_field_lookup_exact_type(self, api: TypeChecker, field: Field) -> MypyType:
|
||||
if isinstance(field, (RelatedField, ForeignObjectRel)):
|
||||
related_model_cls = field.related_model
|
||||
primary_key_field = self.get_primary_key_field(related_model_cls)
|
||||
primary_key_type = self.fields_context.get_field_get_type(api, primary_key_field, method='init')
|
||||
|
||||
rel_model_info = helpers.lookup_class_typeinfo(api, related_model_cls)
|
||||
if rel_model_info is None:
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
|
||||
model_and_primary_key_type = UnionType.make_union([Instance(rel_model_info, []),
|
||||
primary_key_type])
|
||||
return helpers.make_optional(model_and_primary_key_type)
|
||||
# return helpers.make_optional(Instance(rel_model_info, []))
|
||||
|
||||
field_info = helpers.lookup_class_typeinfo(api, field.__class__)
|
||||
if field_info is None:
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
return helpers.get_private_descriptor_type(field_info, '_pyi_lookup_exact_type',
|
||||
is_nullable=field.null)
|
||||
|
||||
def get_primary_key_field(self, model_cls: Type[Model]) -> Field:
|
||||
for field in model_cls._meta.get_fields():
|
||||
if isinstance(field, Field):
|
||||
|
||||
@@ -1,6 +1,11 @@
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING, Any, Dict, List, Optional, Set, Union, cast
|
||||
from typing import (
|
||||
TYPE_CHECKING, Any, Dict, Iterable, Iterator, List, Optional, Set, Union, cast,
|
||||
)
|
||||
|
||||
from django.db.models.fields import Field
|
||||
from django.db.models.fields.related import RelatedField
|
||||
from django.db.models.fields.reverse_related import ForeignObjectRel
|
||||
from mypy import checker
|
||||
from mypy.checker import TypeChecker
|
||||
from mypy.mro import calculate_mro
|
||||
@@ -115,29 +120,50 @@ def parse_bool(expr: Expression) -> Optional[bool]:
|
||||
return None
|
||||
|
||||
|
||||
def has_any_of_bases(info: TypeInfo, bases: Set[str]) -> bool:
|
||||
def has_any_of_bases(info: TypeInfo, bases: Iterable[str]) -> bool:
|
||||
for base_fullname in bases:
|
||||
if info.has_base(base_fullname):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def iter_bases(info: TypeInfo) -> Iterator[Instance]:
|
||||
for base in info.bases:
|
||||
yield base
|
||||
yield from iter_bases(base.type)
|
||||
|
||||
|
||||
def get_private_descriptor_type(type_info: TypeInfo, private_field_name: str, is_nullable: bool) -> MypyType:
|
||||
""" Return declared type of type_info's private_field_name (used for private Field attributes)"""
|
||||
sym = type_info.get(private_field_name)
|
||||
if sym is None:
|
||||
return AnyType(TypeOfAny.unannotated)
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
|
||||
node = sym.node
|
||||
if isinstance(node, Var):
|
||||
descriptor_type = node.type
|
||||
if descriptor_type is None:
|
||||
return AnyType(TypeOfAny.unannotated)
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
|
||||
if is_nullable:
|
||||
descriptor_type = make_optional(descriptor_type)
|
||||
return descriptor_type
|
||||
return AnyType(TypeOfAny.unannotated)
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
|
||||
|
||||
def get_field_lookup_exact_type(api: TypeChecker, field: Field) -> MypyType:
|
||||
if isinstance(field, (RelatedField, ForeignObjectRel)):
|
||||
lookup_type_class = field.related_model
|
||||
rel_model_info = lookup_class_typeinfo(api, lookup_type_class)
|
||||
if rel_model_info is None:
|
||||
return AnyType(TypeOfAny.from_error)
|
||||
return make_optional(Instance(rel_model_info, []))
|
||||
|
||||
field_info = lookup_class_typeinfo(api, field.__class__)
|
||||
if field_info is None:
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
return get_private_descriptor_type(field_info, '_pyi_lookup_exact_type',
|
||||
is_nullable=field.null)
|
||||
|
||||
|
||||
def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]:
|
||||
|
||||
@@ -209,6 +209,8 @@ class NewSemanalDjangoPlugin(Plugin):
|
||||
manager_classes = self._get_current_manager_bases()
|
||||
if class_fullname in manager_classes and method_name == 'create':
|
||||
return partial(init_create.redefine_and_typecheck_model_create, django_context=self.django_context)
|
||||
if class_fullname in manager_classes and method_name in {'filter', 'get', 'exclude'}:
|
||||
return partial(init_create.typecheck_queryset_filter, django_context=self.django_context)
|
||||
return None
|
||||
|
||||
def get_base_class_hook(self, fullname: str
|
||||
|
||||
@@ -6,7 +6,7 @@ from mypy.types import Instance
|
||||
from mypy.types import Type as MypyType
|
||||
|
||||
from mypy_django_plugin.django.context import DjangoContext
|
||||
from mypy_django_plugin.lib import helpers
|
||||
from mypy_django_plugin.lib import fullnames, helpers
|
||||
|
||||
|
||||
def get_actual_types(ctx: Union[MethodContext, FunctionContext],
|
||||
@@ -30,6 +30,12 @@ def get_actual_types(ctx: Union[MethodContext, FunctionContext],
|
||||
return actual_types
|
||||
|
||||
|
||||
def check_types_compatible(ctx, *, expected_type, actual_type, error_message):
|
||||
ctx.api.check_subtype(actual_type, expected_type,
|
||||
ctx.context, error_message,
|
||||
'got', 'expected')
|
||||
|
||||
|
||||
def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_context: DjangoContext,
|
||||
model_cls: Type[Model], method: str) -> MypyType:
|
||||
typechecker_api = helpers.get_typechecker_api(ctx)
|
||||
@@ -42,11 +48,11 @@ def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_co
|
||||
model_cls.__name__),
|
||||
ctx.context)
|
||||
continue
|
||||
typechecker_api.check_subtype(actual_type, expected_types[actual_name],
|
||||
ctx.context,
|
||||
'Incompatible type for "{}" of "{}"'.format(actual_name,
|
||||
model_cls.__name__),
|
||||
'got', 'expected')
|
||||
check_types_compatible(ctx,
|
||||
expected_type=expected_types[actual_name],
|
||||
actual_type=actual_type,
|
||||
error_message='Incompatible type for "{}" of "{}"'.format(actual_name,
|
||||
model_cls.__name__))
|
||||
|
||||
return ctx.default_return_type
|
||||
|
||||
@@ -73,3 +79,40 @@ def redefine_and_typecheck_model_create(ctx: MethodContext, django_context: Djan
|
||||
return ctx.default_return_type
|
||||
|
||||
return typecheck_model_method(ctx, django_context, model_cls, 'create')
|
||||
|
||||
|
||||
def typecheck_queryset_filter(ctx: MethodContext, django_context: DjangoContext) -> MypyType:
|
||||
lookup_kwargs = ctx.arg_names[1]
|
||||
provided_lookup_types = ctx.arg_types[1]
|
||||
|
||||
assert isinstance(ctx.type, Instance)
|
||||
|
||||
if not ctx.type.args or not isinstance(ctx.type.args[0], Instance):
|
||||
return ctx.default_return_type
|
||||
|
||||
model_cls_fullname = ctx.type.args[0].type.fullname()
|
||||
model_cls = django_context.get_model_class_by_fullname(model_cls_fullname)
|
||||
if model_cls is None:
|
||||
return ctx.default_return_type
|
||||
|
||||
for lookup_kwarg, provided_type in zip(lookup_kwargs, provided_lookup_types):
|
||||
if lookup_kwarg is None:
|
||||
continue
|
||||
# Combinables are not supported yet
|
||||
if (isinstance(provided_type, Instance)
|
||||
and provided_type.type.has_base('django.db.models.expressions.Combinable')):
|
||||
continue
|
||||
|
||||
lookup_type = django_context.lookups_context.resolve_lookup_expected_type(ctx, model_cls, lookup_kwarg)
|
||||
# Managers as provided_type is not supported yet
|
||||
if (isinstance(provided_type, Instance)
|
||||
and helpers.has_any_of_bases(provided_type.type, (fullnames.MANAGER_CLASS_FULLNAME,
|
||||
fullnames.QUERYSET_CLASS_FULLNAME))):
|
||||
return ctx.default_return_type
|
||||
|
||||
check_types_compatible(ctx,
|
||||
expected_type=lookup_type,
|
||||
actual_type=provided_type,
|
||||
error_message=f'Incompatible type for lookup {lookup_kwarg!r}:')
|
||||
|
||||
return ctx.default_return_type
|
||||
|
||||
@@ -10,7 +10,9 @@ from mypy.types import AnyType, Instance
|
||||
from mypy.types import Type as MypyType
|
||||
from mypy.types import TypeOfAny
|
||||
|
||||
from mypy_django_plugin.django.context import DjangoContext
|
||||
from mypy_django_plugin.django.context import (
|
||||
DjangoContext, LookupsAreUnsupported,
|
||||
)
|
||||
from mypy_django_plugin.lib import fullnames, helpers
|
||||
|
||||
|
||||
@@ -38,10 +40,12 @@ def determine_proper_manager_type(ctx: FunctionContext) -> MypyType:
|
||||
def get_field_type_from_lookup(ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model],
|
||||
*, method: str, lookup: str) -> Optional[MypyType]:
|
||||
try:
|
||||
lookup_field = django_context.lookups_context.resolve_lookup(model_cls, lookup)
|
||||
lookup_field = django_context.lookups_context.resolve_lookup_info_field(model_cls, lookup)
|
||||
except FieldError as exc:
|
||||
ctx.api.fail(exc.args[0], ctx.context)
|
||||
return None
|
||||
except LookupsAreUnsupported:
|
||||
return AnyType(TypeOfAny.explicit)
|
||||
|
||||
if isinstance(lookup_field, RelatedField) and lookup_field.column == lookup:
|
||||
related_model_cls = django_context.fields_context.get_related_model_cls(lookup_field)
|
||||
|
||||
@@ -46,7 +46,6 @@ IGNORED_ERRORS = {
|
||||
'"Handler" has no attribute',
|
||||
'Module has no attribute',
|
||||
'namedtuple',
|
||||
'Lookups not supported yet',
|
||||
# TODO: see test in managers/test_managers.yml
|
||||
"Cannot determine type of",
|
||||
'cache_clear',
|
||||
@@ -107,6 +106,9 @@ IGNORED_ERRORS = {
|
||||
'custom_lookups': [
|
||||
'in base class "SQLFuncMixin"'
|
||||
],
|
||||
'custom_columns': [
|
||||
"Cannot resolve keyword 'firstname' into field",
|
||||
],
|
||||
'custom_pk': [
|
||||
'"Employee" has no attribute "id"'
|
||||
],
|
||||
@@ -125,6 +127,9 @@ IGNORED_ERRORS = {
|
||||
'defer': [
|
||||
'Too many arguments for "refresh_from_db" of "Model"'
|
||||
],
|
||||
'delete': [
|
||||
'Incompatible type for lookup \'pk\': (got "Optional[int]", expected "int")',
|
||||
],
|
||||
'dispatch': [
|
||||
'Item "str" of "Union[ValueError, str]" has no attribute "args"'
|
||||
],
|
||||
@@ -165,6 +170,9 @@ IGNORED_ERRORS = {
|
||||
+ 'expected "Union[Type[<nothing>], QuerySet[<nothing>]]"',
|
||||
'CustomClass'
|
||||
],
|
||||
'generic_relations': [
|
||||
"Cannot resolve keyword 'vegetable' into field"
|
||||
],
|
||||
'generic_relations_regress': [
|
||||
'"Link" has no attribute'
|
||||
],
|
||||
@@ -178,7 +186,12 @@ IGNORED_ERRORS = {
|
||||
],
|
||||
'lookup': [
|
||||
'Unexpected keyword argument "headline__startswith" for "in_bulk" of',
|
||||
'is called with more than one field'
|
||||
'is called with more than one field',
|
||||
"Cannot resolve keyword 'pub_date_year' into field",
|
||||
"Cannot resolve keyword 'blahblah' into field",
|
||||
],
|
||||
'm2m_regress': [
|
||||
"Cannot resolve keyword 'porcupine' into field",
|
||||
],
|
||||
'messages_tests': [
|
||||
'List item 0 has incompatible type "Dict[str, Message]"; expected "Message"'
|
||||
@@ -208,7 +221,8 @@ IGNORED_ERRORS = {
|
||||
'base class "AbstractModel" defined',
|
||||
'Definition of "name" in base class "ConcreteParent"',
|
||||
' Definition of "name" in base class "AbstractParent"',
|
||||
'referent_references'
|
||||
'referent_references',
|
||||
"Cannot resolve keyword 'attached_comment_set' into field"
|
||||
],
|
||||
'model_meta': [
|
||||
'List item 0 has incompatible type "str"; expected "Union[Field[Any, Any], ForeignObjectRel]"'
|
||||
@@ -221,6 +235,9 @@ IGNORED_ERRORS = {
|
||||
'multiple_database': [
|
||||
'Unexpected attribute "extra_arg" for model "Book"'
|
||||
],
|
||||
'null_queries': [
|
||||
"Cannot resolve keyword 'foo' into field"
|
||||
],
|
||||
'order_with_respect_to': [
|
||||
'BaseOrderWithRespectToTests',
|
||||
'"Dimension" has no attribute "set_component_order"',
|
||||
@@ -265,7 +282,9 @@ IGNORED_ERRORS = {
|
||||
'Unsupported operand types for | ("Manager[Author]" and "Manager[Tag]")',
|
||||
'ObjectA',
|
||||
"'flat' and 'named' can't be used together",
|
||||
'"Collection[Any]" has no attribute "explain"'
|
||||
'"Collection[Any]" has no attribute "explain"',
|
||||
"Cannot resolve keyword 'unknown_field' into field",
|
||||
'Incompatible type for lookup \'tag\': (got "str", expected "Union[Tag, int, None]")',
|
||||
],
|
||||
'requests': [
|
||||
'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")'
|
||||
@@ -273,6 +292,9 @@ IGNORED_ERRORS = {
|
||||
'responses': [
|
||||
'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"'
|
||||
],
|
||||
'reverse_lookup': [
|
||||
"Cannot resolve keyword 'choice' into field"
|
||||
],
|
||||
'settings_tests': [
|
||||
'Argument 1 to "Settings" has incompatible type "Optional[str]"; expected "str"'
|
||||
],
|
||||
|
||||
215
test-data/typecheck/managers/querysets/test_filter.yml
Normal file
215
test-data/typecheck/managers/querysets/test_filter.yml
Normal file
@@ -0,0 +1,215 @@
|
||||
- case: filtering_with_proper_types
|
||||
main: |
|
||||
from myapp.models import User
|
||||
User.objects.filter(username='maksim')
|
||||
User.objects.get(username='maksim')
|
||||
User.objects.exclude(username='maksim')
|
||||
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: no_such_field_for_filter
|
||||
main: |
|
||||
from myapp.models import User
|
||||
User.objects.filter(unknown_field=True) # E: Cannot resolve keyword 'unknown_field' into field. Choices are: id
|
||||
installed_apps:
|
||||
- myapp
|
||||
files:
|
||||
- path: myapp/__init__.py
|
||||
- path: myapp/models.py
|
||||
content: |
|
||||
from django.db import models
|
||||
class User(models.Model):
|
||||
pass
|
||||
|
||||
|
||||
- case: filter_with_invalid_type
|
||||
main: |
|
||||
from myapp.models import User
|
||||
User.objects.filter(age='hello') # E: Incompatible type for lookup 'age': (got "str", expected "int")
|
||||
installed_apps:
|
||||
- myapp
|
||||
files:
|
||||
- path: myapp/__init__.py
|
||||
- path: myapp/models.py
|
||||
content: |
|
||||
from django.db import models
|
||||
from django.db import models
|
||||
class User(models.Model):
|
||||
age = models.IntegerField()
|
||||
|
||||
|
||||
- case: filter_with_multiple_fields
|
||||
main: |
|
||||
from myapp.models import User
|
||||
User.objects.filter(age='hello', gender='world')
|
||||
installed_apps:
|
||||
- myapp
|
||||
out: |
|
||||
main:2: error: Incompatible type for lookup 'age': (got "str", expected "int")
|
||||
main:2: error: Incompatible type for lookup 'gender': (got "str", expected "int")
|
||||
files:
|
||||
- path: myapp/__init__.py
|
||||
- path: myapp/models.py
|
||||
content: |
|
||||
from django.db import models
|
||||
class User(models.Model):
|
||||
age = models.IntegerField()
|
||||
gender = models.IntegerField()
|
||||
|
||||
|
||||
- case: valid_filter_with_lookup
|
||||
main: |
|
||||
from myapp.models import User
|
||||
User.objects.filter(username__contains='hello')
|
||||
User.objects.filter(age__gt=1)
|
||||
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)
|
||||
age = models.IntegerField()
|
||||
|
||||
|
||||
- case: invalid_filter_with_lookup
|
||||
main: |
|
||||
from myapp.models import User
|
||||
User.objects.filter(username__contains=1) # E: Incompatible type for lookup 'username__contains': (got "int", expected "str")
|
||||
User.objects.filter(username__icontains=1) # E: Incompatible type for lookup 'username__icontains': (got "int", expected "str")
|
||||
User.objects.filter(username__isnull=1) # E: Incompatible type for lookup 'username__isnull': (got "int", expected "bool")
|
||||
|
||||
User.objects.filter(created_at=User()) # E: Incompatible type for lookup 'created_at': (got "User", expected "Union[str, datetime]")
|
||||
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)
|
||||
age = models.IntegerField()
|
||||
created_at = models.DateTimeField()
|
||||
|
||||
- case: strings_are_allowed_for_exact_for_dates
|
||||
main: |
|
||||
from myapp.models import User
|
||||
User.objects.filter(created_at='2018')
|
||||
User.objects.filter(created_at__exact='2018')
|
||||
installed_apps:
|
||||
- myapp
|
||||
files:
|
||||
- path: myapp/__init__.py
|
||||
- path: myapp/models.py
|
||||
content: |
|
||||
from django.db import models
|
||||
class User(models.Model):
|
||||
created_at = models.DateTimeField()
|
||||
|
||||
|
||||
- case: related_model_foreign_key_lookups
|
||||
main: |
|
||||
from myapp.models import Blog, Publisher
|
||||
blog = Blog()
|
||||
publisher = Publisher()
|
||||
Blog.objects.filter(publisher=publisher)
|
||||
Blog.objects.filter(publisher_id=1)
|
||||
Blog.objects.filter(publisher__id=1)
|
||||
|
||||
Blog.objects.filter(publisher=blog) # E: Incompatible type for lookup 'publisher': (got "Blog", expected "Union[Publisher, int, None]")
|
||||
Blog.objects.filter(publisher_id='hello') # E: Incompatible type for lookup 'publisher_id': (got "str", expected "int")
|
||||
installed_apps:
|
||||
- myapp
|
||||
files:
|
||||
- path: myapp/__init__.py
|
||||
- path: myapp/models.py
|
||||
content: |
|
||||
from django.db import models
|
||||
class Publisher(models.Model):
|
||||
pass
|
||||
class Blog(models.Model):
|
||||
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE, related_name='blogs')
|
||||
|
||||
|
||||
- case: related_model_reverse_foreign_key_lookup
|
||||
main: |
|
||||
from myapp.models import Blog, Publisher
|
||||
blog = Blog()
|
||||
publisher = Publisher()
|
||||
Publisher.objects.filter(blogs=Blog())
|
||||
Publisher.objects.filter(blogs__id=1)
|
||||
|
||||
Publisher.objects.filter(blogs=publisher) # E: Incompatible type for lookup 'blogs': (got "Publisher", expected "Union[Blog, int, None]")
|
||||
Publisher.objects.filter(blogs__id=publisher) # E: Incompatible type for lookup 'blogs__id': (got "Publisher", expected "int")
|
||||
installed_apps:
|
||||
- myapp
|
||||
files:
|
||||
- path: myapp/__init__.py
|
||||
- path: myapp/models.py
|
||||
content: |
|
||||
from django.db import models
|
||||
class Publisher(models.Model):
|
||||
pass
|
||||
class Blog(models.Model):
|
||||
publisher = models.ForeignKey(Publisher, on_delete=models.CASCADE, related_name='blogs')
|
||||
|
||||
|
||||
- case: many_to_many_lookups
|
||||
main: |
|
||||
from myapp.models import Book, Author
|
||||
book = Book()
|
||||
author = Author()
|
||||
|
||||
Book.objects.filter(authors=author)
|
||||
Book.objects.filter(authors=book) # E: Incompatible type for lookup 'authors': (got "Book", expected "Union[Author, int, None]")
|
||||
Book.objects.filter(authors='hello') # E: Incompatible type for lookup 'authors': (got "str", expected "Union[Author, int, None]")
|
||||
|
||||
Author.objects.filter(books=book)
|
||||
Author.objects.filter(books=author) # E: Incompatible type for lookup 'books': (got "Author", expected "Union[Book, int, None]")
|
||||
Author.objects.filter(books='hello') # E: Incompatible type for lookup 'books': (got "str", expected "Union[Book, int, None]")
|
||||
installed_apps:
|
||||
- myapp
|
||||
files:
|
||||
- path: myapp/__init__.py
|
||||
- path: myapp/models.py
|
||||
content: |
|
||||
from django.db import models
|
||||
class Book(models.Model):
|
||||
pass
|
||||
class Author(models.Model):
|
||||
books = models.ManyToManyField(Book, related_name='authors')
|
||||
|
||||
|
||||
- case: one_to_one_lookups
|
||||
main: |
|
||||
from myapp.models import User, Profile
|
||||
user = User()
|
||||
profile = Profile()
|
||||
User.objects.filter(profile=profile)
|
||||
User.objects.filter(profile=user) # E: Incompatible type for lookup 'profile': (got "User", expected "Union[Profile, int, None]")
|
||||
User.objects.filter(profile='hello') # E: Incompatible type for lookup 'profile': (got "str", expected "Union[Profile, int, None]")
|
||||
Profile.objects.filter(user=user)
|
||||
Profile.objects.filter(user=profile) # E: Incompatible type for lookup 'user': (got "Profile", expected "Union[User, int, None]")
|
||||
Profile.objects.filter(user='hello') # E: Incompatible type for lookup 'user': (got "str", expected "Union[User, int, None]")
|
||||
installed_apps:
|
||||
- myapp
|
||||
files:
|
||||
- path: myapp/__init__.py
|
||||
- path: myapp/models.py
|
||||
content: |
|
||||
from django.db import models
|
||||
class User(models.Model):
|
||||
pass
|
||||
class Profile(models.Model):
|
||||
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
|
||||
@@ -145,8 +145,7 @@
|
||||
main:3: note: Revealed type is 'Any'
|
||||
main:4: error: Cannot resolve keyword 'unknown' into field. Choices are: id, publisher, publisher_id
|
||||
main:4: note: Revealed type is 'Any'
|
||||
main:5: error: Lookups not supported yet
|
||||
main:5: note: Revealed type is 'Any'
|
||||
main:5: note: Revealed type is 'Tuple[Any]'
|
||||
installed_apps:
|
||||
- myapp
|
||||
files:
|
||||
|
||||
Reference in New Issue
Block a user