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:
Maxim Kurnikov
2019-09-30 03:05:40 +03:00
committed by GitHub
parent 4d4b0003bd
commit 02bdf5be95
10 changed files with 451 additions and 48 deletions

View File

@@ -43,6 +43,7 @@ _GT = TypeVar("_GT")
class Field(RegisterLookupMixin, Generic[_ST, _GT]): class Field(RegisterLookupMixin, Generic[_ST, _GT]):
_pyi_private_set_type: Any _pyi_private_set_type: Any
_pyi_private_get_type: Any _pyi_private_get_type: Any
_pyi_lookup_exact_type: Any
widget: Widget widget: Widget
help_text: str help_text: str
@@ -131,6 +132,7 @@ class Field(RegisterLookupMixin, Generic[_ST, _GT]):
class IntegerField(Field[_ST, _GT]): class IntegerField(Field[_ST, _GT]):
_pyi_private_set_type: Union[float, int, str, Combinable] _pyi_private_set_type: Union[float, int, str, Combinable]
_pyi_private_get_type: int _pyi_private_get_type: int
_pyi_lookup_exact_type: int
class PositiveIntegerRelDbTypeMixin: class PositiveIntegerRelDbTypeMixin:
def rel_db_type(self, connection: Any): ... def rel_db_type(self, connection: Any): ...
@@ -143,10 +145,12 @@ class BigIntegerField(IntegerField[_ST, _GT]): ...
class FloatField(Field[_ST, _GT]): class FloatField(Field[_ST, _GT]):
_pyi_private_set_type: Union[float, int, str, Combinable] _pyi_private_set_type: Union[float, int, str, Combinable]
_pyi_private_get_type: float _pyi_private_get_type: float
_pyi_lookup_exact_type: float
class DecimalField(Field[_ST, _GT]): class DecimalField(Field[_ST, _GT]):
_pyi_private_set_type: Union[str, float, decimal.Decimal, Combinable] _pyi_private_set_type: Union[str, float, decimal.Decimal, Combinable]
_pyi_private_get_type: decimal.Decimal _pyi_private_get_type: decimal.Decimal
_pyi_lookup_exact_type: Union[str, decimal.Decimal]
# attributes # attributes
max_digits: int = ... max_digits: int = ...
decimal_places: int = ... decimal_places: int = ...
@@ -176,10 +180,13 @@ class DecimalField(Field[_ST, _GT]):
class AutoField(Field[_ST, _GT]): class AutoField(Field[_ST, _GT]):
_pyi_private_set_type: Union[Combinable, int, str] _pyi_private_set_type: Union[Combinable, int, str]
_pyi_private_get_type: int _pyi_private_get_type: int
_pyi_lookup_exact_type: int
class CharField(Field[_ST, _GT]): class CharField(Field[_ST, _GT]):
_pyi_private_set_type: Union[str, int, Combinable] _pyi_private_set_type: Union[str, int, Combinable]
_pyi_private_get_type: str _pyi_private_get_type: str
# objects are converted to string before comparison
_pyi_lookup_exact_type: Any
def __init__( def __init__(
self, self,
verbose_name: Optional[Union[str, bytes]] = ..., verbose_name: Optional[Union[str, bytes]] = ...,
@@ -238,14 +245,18 @@ class URLField(CharField[_ST, _GT]): ...
class TextField(Field[_ST, _GT]): class TextField(Field[_ST, _GT]):
_pyi_private_set_type: Union[str, Combinable] _pyi_private_set_type: Union[str, Combinable]
_pyi_private_get_type: str _pyi_private_get_type: str
# objects are converted to string before comparison
_pyi_lookup_exact_type: Any
class BooleanField(Field[_ST, _GT]): class BooleanField(Field[_ST, _GT]):
_pyi_private_set_type: Union[bool, Combinable] _pyi_private_set_type: Union[bool, Combinable]
_pyi_private_get_type: bool _pyi_private_get_type: bool
_pyi_lookup_exact_type: bool
class NullBooleanField(Field[_ST, _GT]): class NullBooleanField(Field[_ST, _GT]):
_pyi_private_set_type: Optional[Union[bool, Combinable]] _pyi_private_set_type: Optional[Union[bool, Combinable]]
_pyi_private_get_type: Optional[bool] _pyi_private_get_type: Optional[bool]
_pyi_lookup_exact_type: Optional[bool]
class IPAddressField(Field[_ST, _GT]): class IPAddressField(Field[_ST, _GT]):
_pyi_private_set_type: Union[str, Combinable] _pyi_private_set_type: Union[str, Combinable]
@@ -286,6 +297,7 @@ class DateTimeCheckMixin: ...
class DateField(DateTimeCheckMixin, Field[_ST, _GT]): class DateField(DateTimeCheckMixin, Field[_ST, _GT]):
_pyi_private_set_type: Union[str, date, Combinable] _pyi_private_set_type: Union[str, date, Combinable]
_pyi_private_get_type: date _pyi_private_get_type: date
_pyi_lookup_exact_type: Union[str, date]
def __init__( def __init__(
self, self,
verbose_name: Optional[Union[str, bytes]] = ..., verbose_name: Optional[Union[str, bytes]] = ...,
@@ -338,6 +350,7 @@ class TimeField(DateTimeCheckMixin, Field[_ST, _GT]):
class DateTimeField(DateField[_ST, _GT]): class DateTimeField(DateField[_ST, _GT]):
_pyi_private_get_type: datetime _pyi_private_get_type: datetime
_pyi_lookup_exact_type: Union[str, datetime]
class UUIDField(Field[_ST, _GT]): class UUIDField(Field[_ST, _GT]):
_pyi_private_set_type: Union[str, uuid.UUID] _pyi_private_set_type: Union[str, uuid.UUID]

View File

@@ -1,5 +1,5 @@
from datetime import datetime 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.backends.sqlite3.base import DatabaseWrapper
from django.db.models.expressions import Expression, Func 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 from django.db.models.fields import TextField, related_lookups
class Lookup: _T = TypeVar("_T")
class Lookup(Generic[_T]):
lookup_name: str = ... lookup_name: str = ...
prepare_rhs: bool = ... prepare_rhs: bool = ...
can_use_none_as_rhs: bool = ... can_use_none_as_rhs: bool = ...
@@ -47,7 +49,7 @@ class Transform(RegisterLookupMixin, Func):
def lhs(self) -> Expression: ... def lhs(self) -> Expression: ...
def get_bilateral_transforms(self) -> List[Type[Transform]]: ... 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: ... def get_rhs_op(self, connection: DatabaseWrapper, rhs: str) -> str: ...
class FieldGetDbPrepValueMixin: class FieldGetDbPrepValueMixin:
@@ -62,21 +64,21 @@ class FieldGetDbPrepValueIterableMixin(FieldGetDbPrepValueMixin):
class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): ... class Exact(FieldGetDbPrepValueMixin, BuiltinLookup): ...
class IExact(BuiltinLookup): ... class IExact(BuiltinLookup): ...
class GreaterThan(FieldGetDbPrepValueMixin, BuiltinLookup): ... class GreaterThan(FieldGetDbPrepValueMixin, BuiltinLookup): ...
class GreaterThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): ... class GreaterThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup[_T]): ...
class LessThan(FieldGetDbPrepValueMixin, BuiltinLookup): ... class LessThan(FieldGetDbPrepValueMixin, BuiltinLookup[_T]): ...
class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): ... class LessThanOrEqual(FieldGetDbPrepValueMixin, BuiltinLookup): ...
class IntegerFieldFloatRounding: class IntegerFieldFloatRounding:
rhs: Any = ... rhs: Any = ...
def get_prep_lookup(self) -> Any: ... def get_prep_lookup(self) -> Any: ...
class IntegerGreaterThanOrEqual(IntegerFieldFloatRounding, GreaterThanOrEqual): ... class IntegerGreaterThanOrEqual(IntegerFieldFloatRounding, GreaterThanOrEqual[Union[int, float]]): ...
class IntegerLessThan(IntegerFieldFloatRounding, LessThan): ... class IntegerLessThan(IntegerFieldFloatRounding, LessThan[Union[int, float]]): ...
class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup): class In(FieldGetDbPrepValueIterableMixin, BuiltinLookup):
def split_parameter_list_as_sql(self, compiler: Any, connection: Any): ... def split_parameter_list_as_sql(self, compiler: Any, connection: Any): ...
class PatternLookup(BuiltinLookup): class PatternLookup(BuiltinLookup[str]):
param_pattern: str = ... param_pattern: str = ...
class Contains(PatternLookup): ... class Contains(PatternLookup): ...
@@ -86,8 +88,8 @@ class IStartsWith(StartsWith): ...
class EndsWith(PatternLookup): ... class EndsWith(PatternLookup): ...
class IEndsWith(EndsWith): ... class IEndsWith(EndsWith): ...
class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup): ... class Range(FieldGetDbPrepValueIterableMixin, BuiltinLookup): ...
class IsNull(BuiltinLookup): ... class IsNull(BuiltinLookup[bool]): ...
class Regex(BuiltinLookup): ... class Regex(BuiltinLookup[str]): ...
class IRegex(Regex): ... class IRegex(Regex): ...
class YearLookup(Lookup): class YearLookup(Lookup):

View File

@@ -2,7 +2,7 @@ import os
from collections import defaultdict from collections import defaultdict
from contextlib import contextmanager from contextlib import contextmanager
from typing import ( 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 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 import AutoField, CharField, Field
from django.db.models.fields.related import ForeignKey, RelatedField from django.db.models.fields.related import ForeignKey, RelatedField
from django.db.models.fields.reverse_related import ForeignObjectRel 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.db.models.sql.query import Query
from django.utils.functional import cached_property from django.utils.functional import cached_property
from mypy.checker import TypeChecker from mypy.checker import TypeChecker
from mypy.plugin import MethodContext
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 TypeOfAny, UnionType
from mypy_django_plugin.lib import helpers from mypy_django_plugin.lib import fullnames, helpers
try: try:
from django.contrib.postgres.fields import ArrayField from django.contrib.postgres.fields import ArrayField
@@ -153,33 +155,87 @@ class DjangoFieldsContext:
return related_model_cls return related_model_cls
class LookupsAreUnsupported(Exception):
pass
class DjangoLookupsContext: class DjangoLookupsContext:
def __init__(self, django_context: 'DjangoContext'): def __init__(self, django_context: 'DjangoContext'):
self.django_context = django_context 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) query = Query(model_cls)
lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup) lookup_parts, field_parts, is_expression = query.solve_lookup_type(lookup)
if lookup_parts: if lookup_parts:
raise FieldError('Lookups not supported yet') raise LookupsAreUnsupported()
currently_observed_model = model_cls return self._resolve_field_from_parts(field_parts, 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)
current_field = currently_observed_model._meta.get_field(field_part) def resolve_lookup_expected_type(self, ctx: MethodContext, model_cls: Type[Model], lookup: str) -> MypyType:
if not isinstance(current_field, (ForeignObjectRel, RelatedField)): query = Query(model_cls)
continue 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) field = self._resolve_field_from_parts(field_parts, model_cls)
if isinstance(current_field, ForeignObjectRel):
current_field = self.django_context.get_primary_key_field(currently_observed_model)
# if it is None, solve_lookup_type() will fail earlier lookup_cls = None
assert current_field is not None if lookup_parts:
return current_field 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: class DjangoContext:
@@ -228,6 +284,27 @@ class DjangoContext:
if isinstance(field, ForeignObjectRel): if isinstance(field, ForeignObjectRel):
yield field 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: def get_primary_key_field(self, model_cls: Type[Model]) -> Field:
for field in model_cls._meta.get_fields(): for field in model_cls._meta.get_fields():
if isinstance(field, Field): if isinstance(field, Field):

View File

@@ -1,6 +1,11 @@
from collections import OrderedDict 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 import checker
from mypy.checker import TypeChecker from mypy.checker import TypeChecker
from mypy.mro import calculate_mro from mypy.mro import calculate_mro
@@ -115,29 +120,50 @@ def parse_bool(expr: Expression) -> Optional[bool]:
return None 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: for base_fullname in bases:
if info.has_base(base_fullname): if info.has_base(base_fullname):
return True return True
return False 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: 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)""" """ Return declared type of type_info's private_field_name (used for private Field attributes)"""
sym = type_info.get(private_field_name) sym = type_info.get(private_field_name)
if sym is None: if sym is None:
return AnyType(TypeOfAny.unannotated) return AnyType(TypeOfAny.explicit)
node = sym.node node = sym.node
if isinstance(node, Var): if isinstance(node, Var):
descriptor_type = node.type descriptor_type = node.type
if descriptor_type is None: if descriptor_type is None:
return AnyType(TypeOfAny.unannotated) return AnyType(TypeOfAny.explicit)
if is_nullable: if is_nullable:
descriptor_type = make_optional(descriptor_type) descriptor_type = make_optional(descriptor_type)
return 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]: def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]:

View File

@@ -209,6 +209,8 @@ class NewSemanalDjangoPlugin(Plugin):
manager_classes = self._get_current_manager_bases() 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'}:
return partial(init_create.typecheck_queryset_filter, django_context=self.django_context)
return None return None
def get_base_class_hook(self, fullname: str def get_base_class_hook(self, fullname: str

View File

@@ -6,7 +6,7 @@ from mypy.types import Instance
from mypy.types import Type as MypyType from mypy.types import Type as MypyType
from mypy_django_plugin.django.context import DjangoContext 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], def get_actual_types(ctx: Union[MethodContext, FunctionContext],
@@ -30,6 +30,12 @@ def get_actual_types(ctx: Union[MethodContext, FunctionContext],
return actual_types 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, def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_context: DjangoContext,
model_cls: Type[Model], method: str) -> MypyType: model_cls: Type[Model], method: str) -> MypyType:
typechecker_api = helpers.get_typechecker_api(ctx) typechecker_api = helpers.get_typechecker_api(ctx)
@@ -42,11 +48,11 @@ def typecheck_model_method(ctx: Union[FunctionContext, MethodContext], django_co
model_cls.__name__), model_cls.__name__),
ctx.context) ctx.context)
continue continue
typechecker_api.check_subtype(actual_type, expected_types[actual_name], check_types_compatible(ctx,
ctx.context, expected_type=expected_types[actual_name],
'Incompatible type for "{}" of "{}"'.format(actual_name, actual_type=actual_type,
model_cls.__name__), error_message='Incompatible type for "{}" of "{}"'.format(actual_name,
'got', 'expected') model_cls.__name__))
return ctx.default_return_type 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 ctx.default_return_type
return typecheck_model_method(ctx, django_context, model_cls, 'create') 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

View File

@@ -10,7 +10,9 @@ 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 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 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], def get_field_type_from_lookup(ctx: MethodContext, django_context: DjangoContext, model_cls: Type[Model],
*, method: str, lookup: str) -> Optional[MypyType]: *, method: str, lookup: str) -> Optional[MypyType]:
try: 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: except FieldError as exc:
ctx.api.fail(exc.args[0], ctx.context) ctx.api.fail(exc.args[0], ctx.context)
return None return None
except LookupsAreUnsupported:
return AnyType(TypeOfAny.explicit)
if isinstance(lookup_field, RelatedField) and lookup_field.column == lookup: if isinstance(lookup_field, RelatedField) and lookup_field.column == lookup:
related_model_cls = django_context.fields_context.get_related_model_cls(lookup_field) related_model_cls = django_context.fields_context.get_related_model_cls(lookup_field)

View File

@@ -46,7 +46,6 @@ IGNORED_ERRORS = {
'"Handler" has no attribute', '"Handler" has no attribute',
'Module has no attribute', 'Module has no attribute',
'namedtuple', 'namedtuple',
'Lookups not supported yet',
# TODO: see test in managers/test_managers.yml # TODO: see test in managers/test_managers.yml
"Cannot determine type of", "Cannot determine type of",
'cache_clear', 'cache_clear',
@@ -107,6 +106,9 @@ IGNORED_ERRORS = {
'custom_lookups': [ 'custom_lookups': [
'in base class "SQLFuncMixin"' 'in base class "SQLFuncMixin"'
], ],
'custom_columns': [
"Cannot resolve keyword 'firstname' into field",
],
'custom_pk': [ 'custom_pk': [
'"Employee" has no attribute "id"' '"Employee" has no attribute "id"'
], ],
@@ -125,6 +127,9 @@ IGNORED_ERRORS = {
'defer': [ 'defer': [
'Too many arguments for "refresh_from_db" of "Model"' 'Too many arguments for "refresh_from_db" of "Model"'
], ],
'delete': [
'Incompatible type for lookup \'pk\': (got "Optional[int]", expected "int")',
],
'dispatch': [ 'dispatch': [
'Item "str" of "Union[ValueError, str]" has no attribute "args"' 'Item "str" of "Union[ValueError, str]" has no attribute "args"'
], ],
@@ -165,6 +170,9 @@ IGNORED_ERRORS = {
+ 'expected "Union[Type[<nothing>], QuerySet[<nothing>]]"', + 'expected "Union[Type[<nothing>], QuerySet[<nothing>]]"',
'CustomClass' 'CustomClass'
], ],
'generic_relations': [
"Cannot resolve keyword 'vegetable' into field"
],
'generic_relations_regress': [ 'generic_relations_regress': [
'"Link" has no attribute' '"Link" has no attribute'
], ],
@@ -178,7 +186,12 @@ IGNORED_ERRORS = {
], ],
'lookup': [ 'lookup': [
'Unexpected keyword argument "headline__startswith" for "in_bulk" of', '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': [ 'messages_tests': [
'List item 0 has incompatible type "Dict[str, Message]"; expected "Message"' 'List item 0 has incompatible type "Dict[str, Message]"; expected "Message"'
@@ -208,7 +221,8 @@ IGNORED_ERRORS = {
'base class "AbstractModel" defined', 'base class "AbstractModel" defined',
'Definition of "name" in base class "ConcreteParent"', 'Definition of "name" in base class "ConcreteParent"',
' Definition of "name" in base class "AbstractParent"', ' Definition of "name" in base class "AbstractParent"',
'referent_references' 'referent_references',
"Cannot resolve keyword 'attached_comment_set' into field"
], ],
'model_meta': [ 'model_meta': [
'List item 0 has incompatible type "str"; expected "Union[Field[Any, Any], ForeignObjectRel]"' 'List item 0 has incompatible type "str"; expected "Union[Field[Any, Any], ForeignObjectRel]"'
@@ -221,6 +235,9 @@ IGNORED_ERRORS = {
'multiple_database': [ 'multiple_database': [
'Unexpected attribute "extra_arg" for model "Book"' 'Unexpected attribute "extra_arg" for model "Book"'
], ],
'null_queries': [
"Cannot resolve keyword 'foo' into field"
],
'order_with_respect_to': [ 'order_with_respect_to': [
'BaseOrderWithRespectToTests', 'BaseOrderWithRespectToTests',
'"Dimension" has no attribute "set_component_order"', '"Dimension" has no attribute "set_component_order"',
@@ -265,7 +282,9 @@ IGNORED_ERRORS = {
'Unsupported operand types for | ("Manager[Author]" and "Manager[Tag]")', 'Unsupported operand types for | ("Manager[Author]" and "Manager[Tag]")',
'ObjectA', 'ObjectA',
"'flat' and 'named' can't be used together", "'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': [ 'requests': [
'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")' 'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")'
@@ -273,6 +292,9 @@ IGNORED_ERRORS = {
'responses': [ 'responses': [
'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"' 'Argument 1 to "TextIOWrapper" has incompatible type "HttpResponse"; expected "IO[bytes]"'
], ],
'reverse_lookup': [
"Cannot resolve keyword 'choice' into field"
],
'settings_tests': [ 'settings_tests': [
'Argument 1 to "Settings" has incompatible type "Optional[str]"; expected "str"' 'Argument 1 to "Settings" has incompatible type "Optional[str]"; expected "str"'
], ],

View 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')

View File

@@ -145,8 +145,7 @@
main:3: note: Revealed type is 'Any' main:3: note: Revealed type is 'Any'
main:4: error: Cannot resolve keyword 'unknown' into field. Choices are: id, publisher, publisher_id main:4: error: Cannot resolve keyword 'unknown' into field. Choices are: id, publisher, publisher_id
main:4: note: Revealed type is 'Any' main:4: note: Revealed type is 'Any'
main:5: error: Lookups not supported yet main:5: note: Revealed type is 'Tuple[Any]'
main:5: note: Revealed type is 'Any'
installed_apps: installed_apps:
- myapp - myapp
files: files: