diff --git a/django-stubs/db/models/fields/__init__.pyi b/django-stubs/db/models/fields/__init__.pyi index 7911488..1e7dd30 100644 --- a/django-stubs/db/models/fields/__init__.pyi +++ b/django-stubs/db/models/fields/__init__.pyi @@ -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] diff --git a/django-stubs/db/models/lookups.pyi b/django-stubs/db/models/lookups.pyi index d99a952..3a248a5 100644 --- a/django-stubs/db/models/lookups.pyi +++ b/django-stubs/db/models/lookups.pyi @@ -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): diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index f56f179..a68cb09 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -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): diff --git a/mypy_django_plugin/lib/helpers.py b/mypy_django_plugin/lib/helpers.py index 77685c1..c1adda3 100644 --- a/mypy_django_plugin/lib/helpers.py +++ b/mypy_django_plugin/lib/helpers.py @@ -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]: diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index 8406248..24fb025 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -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 diff --git a/mypy_django_plugin/transformers/init_create.py b/mypy_django_plugin/transformers/init_create.py index d143234..33eb13f 100644 --- a/mypy_django_plugin/transformers/init_create.py +++ b/mypy_django_plugin/transformers/init_create.py @@ -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 diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py index 190e822..93c7d97 100644 --- a/mypy_django_plugin/transformers/querysets.py +++ b/mypy_django_plugin/transformers/querysets.py @@ -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) diff --git a/scripts/enabled_test_modules.py b/scripts/enabled_test_modules.py index e9a9235..44f5a28 100644 --- a/scripts/enabled_test_modules.py +++ b/scripts/enabled_test_modules.py @@ -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[], QuerySet[]]"', '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"' ], diff --git a/test-data/typecheck/managers/querysets/test_filter.yml b/test-data/typecheck/managers/querysets/test_filter.yml new file mode 100644 index 0000000..af01853 --- /dev/null +++ b/test-data/typecheck/managers/querysets/test_filter.yml @@ -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') \ No newline at end of file diff --git a/test-data/typecheck/managers/querysets/test_values_list.yml b/test-data/typecheck/managers/querysets/test_values_list.yml index 34dceb2..b7d5db4 100644 --- a/test-data/typecheck/managers/querysets/test_values_list.yml +++ b/test-data/typecheck/managers/querysets/test_values_list.yml @@ -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: