Skip to content

52/model subtypes dont typecheck #55

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 2 additions & 3 deletions django-stubs/core/serializers/__init__.pyi
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
from collections import OrderedDict
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union, Iterable

from django.apps.config import AppConfig
from django.db.models.base import Model
Expand Down Expand Up @@ -33,5 +32,5 @@ def serialize(
) -> Optional[Union[bytes, str]]: ...
def deserialize(format: str, stream_or_string: Any, **options: Any) -> Union[Iterator[Any], Deserializer]: ...
def sort_dependencies(
app_list: Union[List[Tuple[AppConfig, None]], List[Tuple[str, List[Type[Model]]]]]
app_list: Union[Iterable[Tuple[AppConfig, None]], Iterable[Tuple[str, Iterable[Type[Model]]]]]
) -> List[Type[Model]]: ...
3 changes: 3 additions & 0 deletions django-stubs/db/models/base.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,15 @@ _Self = TypeVar("_Self", bound="Model")

class Model(metaclass=ModelBase):
class DoesNotExist(Exception): ...
class MultipleObjectsReturned(Exception): ...
class Meta: ...
_meta: Any
_default_manager: Manager[Model]
pk: Any = ...
def __init__(self: _Self, *args, **kwargs) -> None: ...
def delete(self, using: Any = ..., keep_parents: bool = ...) -> Tuple[int, Dict[str, int]]: ...
def full_clean(self, exclude: Optional[List[str]] = ..., validate_unique: bool = ...) -> None: ...
def clean(self) -> None: ...
def clean_fields(self, exclude: List[str] = ...) -> None: ...
def validate_unique(self, exclude: List[str] = ...) -> None: ...
def save(
Expand All @@ -34,6 +36,7 @@ class Model(metaclass=ModelBase):
): ...
def refresh_from_db(self: _Self, using: Optional[str] = ..., fields: Optional[List[str]] = ...) -> _Self: ...
def get_deferred_fields(self) -> Set[str]: ...
def __getstate__(self) -> dict: ...

class ModelStateFieldsCacheDescriptor: ...

Expand Down
17 changes: 15 additions & 2 deletions mypy_django_plugin/transformers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from mypy.nodes import (
ARG_STAR, ARG_STAR2, MDEF, Argument, CallExpr, ClassDef, Expression, IndexExpr, Lvalue, MemberExpr, MypyFile,
NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var,
)
ARG_POS)
from mypy.plugin import ClassDefContext
from mypy.plugins.common import add_method
from mypy.semanal import SemanticAnalyzerPass2
Expand Down Expand Up @@ -255,10 +255,23 @@ def add_dummy_init_method(ctx: ClassDefContext) -> None:
type_annotation=any, initializer=None, kind=ARG_STAR2)

add_method(ctx, '__init__', [pos_arg, kw_arg], NoneTyp())

# mark as model class
ctx.cls.info.metadata.setdefault('django', {})['generated_init'] = True


def add_get_set_attr_fallback_to_any(ctx: ClassDefContext):
any = AnyType(TypeOfAny.special_form)

name_arg = Argument(variable=Var('name', any),
type_annotation=any, initializer=None, kind=ARG_POS)
add_method(ctx, '__getattr__', [name_arg], any)

value_arg = Argument(variable=Var('value', any),
type_annotation=any, initializer=None, kind=ARG_POS)
add_method(ctx, '__setattr__', [name_arg, value_arg], any)


def process_model_class(ctx: ClassDefContext) -> None:
initializers = [
InjectAnyAsBaseForNestedMeta,
Expand All @@ -273,4 +286,4 @@ def process_model_class(ctx: ClassDefContext) -> None:
add_dummy_init_method(ctx)

# allow unspecified attributes for now
ctx.cls.info.fallback_to_any = True
add_get_set_attr_fallback_to_any(ctx)
42 changes: 34 additions & 8 deletions scripts/typecheck_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,12 @@
'Argument 1 to "loads" has incompatible type "Union[bytes, str, None]"; '
+ 'expected "Union[str, bytes, bytearray]"',
'Incompatible types in assignment (expression has type "None", variable has type Module)',
'note:'
'note:',
# Suppress false-positive error due to mypy being overly strict with base class compatibility checks even though
# objects/_default_manager are redefined in the subclass to be compatible with the base class definition.
# Can be removed when mypy issue is fixed: https://github.com/python/mypy/issues/2619
re.compile(r'Definition of "(objects|_default_manager)" in base class "[A-Za-z0-9]+" is incompatible with '
r'definition in base class "[A-Za-z0-9]+"'),
],
'admin_changelist': [
'Incompatible types in assignment (expression has type "FilteredChildAdmin", variable has type "ChildAdmin")'
Expand Down Expand Up @@ -213,11 +218,12 @@
'Unexpected keyword argument "headline__startswith" for "in_bulk" of "QuerySet"',
],
'many_to_one': [
'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")'
'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")',
'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")'
],
'model_meta': [
'"object" has no attribute "items"',
'"Field" has no attribute "many_to_many"'
'"Field" has no attribute "many_to_many"',
],
'model_forms': [
'Argument "instance" to "InvalidModelForm" has incompatible type "Type[Category]"; expected "Optional[Model]"',
Expand All @@ -227,8 +233,14 @@
'Incompatible types in assignment (expression has type "Type[Person]", variable has type',
'Unexpected keyword argument "name" for "Person"',
'Cannot assign multiple types to name "PersonTwoImages" without an explicit "Type[...]" annotation',
'Incompatible types in assignment (expression has type "Type[Person]", '
+ 'base class "ImageFieldTestMixin" defined the type as "Type[PersonWithHeightAndWidth]")',
re.compile(
r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class "IntegerFieldTests"'
r' defined the type as "Type\[IntegerModel\]"\)'),
re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class'
r' "ImageFieldTestMixin" defined the type as "Type\[PersonWithHeightAndWidth\]"\)'),
'Incompatible import of "Person"',
'Incompatible types in assignment (expression has type "FloatModel", variable has type '
'"Union[float, int, str, Combinable]")',
],
'model_formsets': [
'Incompatible types in string interpolation (expression has type "object", '
Expand Down Expand Up @@ -261,7 +273,7 @@
'Argument 1 to "RunPython" has incompatible type "str"; expected "Callable[..., Any]"',
'FakeLoader',
'Argument 1 to "append" of "list" has incompatible type "AddIndex"; expected "CreateModel"',
'Unsupported operand types for - ("Set[Any]" and "None")'
'Unsupported operand types for - ("Set[Any]" and "None")',
],
'middleware_exceptions': [
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any]"; expected "str"'
Expand All @@ -282,7 +294,11 @@
+ 'expected "Optional[Type[JSONEncoder]]"',
'for model "CITestModel"',
'Incompatible type for "field" of "IntegerArrayModel" (got "None", '
+ 'expected "Union[Sequence[int], Combinable]")'
+ 'expected "Union[Sequence[int], Combinable]")',
re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class "UnaccentTest" '
r'defined the type as "Type\[CharFieldModel\]"\)'),
'Incompatible types in assignment (expression has type "Type[TextFieldModel]", base class "TrigramTest" '
'defined the type as "Type[CharFieldModel]")',
],
'properties': [
re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"')
Expand All @@ -291,6 +307,12 @@
'Incompatible types in assignment (expression has type "None", variable has type "str")',
'Invalid index type "Optional[str]" for "Dict[str, int]"; expected type "str"',
'No overload variant of "values_list" of "QuerySet" matches argument types "str", "bool", "bool"',
'Unsupported operand types for & ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")',
'Unsupported operand types for | ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")',
'Incompatible types in assignment (expression has type "ObjectB", variable has type "ObjectA")',
'Incompatible types in assignment (expression has type "ObjectC", variable has type "ObjectA")',
'Incompatible type for "objectb" of "ObjectC" (got "ObjectA", expected'
' "Union[ObjectB, Combinable, None, None]")',
],
'requests': [
'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")'
Expand All @@ -303,6 +325,9 @@
'"None" has no attribute "__iter__"',
'has no attribute "read_by"'
],
'proxy_model_inheritance': [
'Incompatible import of "ProxyModel"'
],
'signals': [
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]"; '
+ 'expected "Tuple[Any, Any, Any]"'
Expand Down Expand Up @@ -387,7 +412,8 @@
'Incompatible types in assignment (expression has type "None", variable has type "int")',
],
'select_related_onetoone': [
'"None" has no attribute'
'"None" has no attribute',
'Incompatible types in assignment (expression has type "Parent2", variable has type "Parent1")',
],
'servers': [
re.compile('Argument [0-9] to "WSGIRequestHandler"')
Expand Down
2 changes: 1 addition & 1 deletion test-data/typecheck/managers.test
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ from django.db import models
class MyModel(models.Model):
authors = models.Manager[MyModel]()
reveal_type(MyModel.authors) # E: Revealed type is 'django.db.models.manager.Manager[main.MyModel]'
reveal_type(MyModel.objects) # E: Revealed type is 'Any'
MyModel.objects # E: "Type[MyModel]" has no attribute "objects"
[out]

[CASE test_model_objects_attribute_present_in_case_of_model_cls_passed_as_generic_parameter]
Expand Down
28 changes: 28 additions & 0 deletions test-data/typecheck/model.test
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[CASE test_model_subtype_relationship_and_getting_and_setting_attributes]
from django.db import models

class A(models.Model):
pass

class B(models.Model):
b_attr = 1
pass

class C(A):
pass

def service(a: A) -> int:
pass

a_instance = A()
b_instance = B()
reveal_type(b_instance.b_attr) # E: Revealed type is 'builtins.int'


reveal_type(b_instance.non_existent_attribute) # E: Revealed type is 'Any'
b_instance.non_existent_attribute = 2

service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A"

c_instance = C()
service(c_instance)
4 changes: 4 additions & 0 deletions test-data/typecheck/model_create.test
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,16 @@ class Parent1(models.Model):
class Parent2(models.Model):
id2 = models.AutoField(primary_key=True)
name2 = models.CharField(max_length=50)

# TODO: Remove the 2 expected errors on the next line once mypy issue https://github.com/python/mypy/issues/2619 is resolved:
class Child1(Parent1, Parent2):
value = models.IntegerField()
class Child4(Child1):
value4 = models.IntegerField()
Child4.objects.create(name1='n1', name2='n2', value=1, value4=4)
[out]
main:10: error: Definition of "objects" in base class "Parent1" is incompatible with definition in base class "Parent2"
main:10: error: Definition of "_default_manager" in base class "Parent1" is incompatible with definition in base class "Parent2"

[CASE optional_primary_key_for_create_is_error]
from django.db import models
Expand Down