Repository: typeddjango/django-stubs

Issue:
Catch type error on django models
It is possible to have type errors for django models?

```python
class A(models.Model):
    pass

class B(models.Model):
    pass

def service(a: A) -> int:
    pass

a_instance = A()
b_instance = B()
service(b)  # no type error
```

It would help a lot if this could be caught earlier on the editor.
Because all models inherit from `models.Model` and 

> Every class is also a valid type. Any instance of a subclass is also compatible with all superclasses

I' m trying to figure if there is an option to detect this errors.






Discussion:

Resolving patch:
diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py
--- a/mypy_django_plugin/transformers/models.py
+++ b/mypy_django_plugin/transformers/models.py
@@ -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
@@ -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,
@@ -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)


Test patch:
diff --git a/scripts/typecheck_tests.py b/scripts/typecheck_tests.py
--- a/scripts/typecheck_tests.py
+++ b/scripts/typecheck_tests.py
@@ -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")'
@@ -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]"',
@@ -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", '
@@ -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"'
@@ -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"')
@@ -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")'
@@ -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]"'
@@ -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"')
diff --git a/test-data/typecheck/managers.test b/test-data/typecheck/managers.test
--- a/test-data/typecheck/managers.test
+++ b/test-data/typecheck/managers.test
@@ -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]
diff --git a/test-data/typecheck/model.test b/test-data/typecheck/model.test
new file mode 100644
--- /dev/null
+++ b/test-data/typecheck/model.test
@@ -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)
diff --git a/test-data/typecheck/model_create.test b/test-data/typecheck/model_create.test
--- a/test-data/typecheck/model_create.test
+++ b/test-data/typecheck/model_create.test
@@ -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