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]):
_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]

View File

@@ -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):

View File

@@ -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):

View File

@@ -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]:

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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"'
],

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: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: