diff --git a/django-stubs/core/serializers/__init__.pyi b/django-stubs/core/serializers/__init__.pyi index c0040e391..d9eab4012 100644 --- a/django-stubs/core/serializers/__init__.pyi +++ b/django-stubs/core/serializers/__init__.pyi @@ -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 @@ -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]]: ... diff --git a/django-stubs/db/models/base.pyi b/django-stubs/db/models/base.pyi index 185333ce8..119e90e6b 100644 --- a/django-stubs/db/models/base.pyi +++ b/django-stubs/db/models/base.pyi @@ -8,6 +8,7 @@ _Self = TypeVar("_Self", bound="Model") class Model(metaclass=ModelBase): class DoesNotExist(Exception): ... + class MultipleObjectsReturned(Exception): ... class Meta: ... _meta: Any _default_manager: Manager[Model] @@ -15,6 +16,7 @@ class Model(metaclass=ModelBase): 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( @@ -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: ... diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index ff872f489..a357fa3b0 100644 --- 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) diff --git a/scripts/typecheck_tests.py b/scripts/typecheck_tests.py index 64a32a527..bd2ed696e 100644 --- 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 index 527671f8f..0a7df1e82 100644 --- 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 index 000000000..84ecf6b03 --- /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 index a688eb9c0..b9bba104c 100644 --- 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