Skip to content

Commit 70a92fd

Browse files
committed
Fix problem where Model instancess are not considered subtypes of each other due to fallback_to_any = True. Fixes #52.
- Added a stub for __getstate__ to Model. - Added a stub for clean() to Model. - Correct arg type for sort_dependencies so they are covariant (Iterable rather than List). Test ignores: - Added some test ignores in cases where a model inherits from 2 different base models. - Added some test ignores for cases that MyPy flags as errors due to variable redefinitions or imports that are incompatible types.
1 parent 9288c34 commit 70a92fd

File tree

7 files changed

+86
-15
lines changed

7 files changed

+86
-15
lines changed

django-stubs/core/serializers/__init__.pyi

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
from collections import OrderedDict
2-
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union
1+
from typing import Any, Callable, Dict, Iterator, List, Optional, Tuple, Type, Union, Iterable
32

43
from django.apps.config import AppConfig
54
from django.db.models.base import Model
@@ -33,5 +32,5 @@ def serialize(
3332
) -> Optional[Union[bytes, str]]: ...
3433
def deserialize(format: str, stream_or_string: Any, **options: Any) -> Union[Iterator[Any], Deserializer]: ...
3534
def sort_dependencies(
36-
app_list: Union[List[Tuple[AppConfig, None]], List[Tuple[str, List[Type[Model]]]]]
35+
app_list: Union[Iterable[Tuple[AppConfig, None]], Iterable[Tuple[str, Iterable[Type[Model]]]]]
3736
) -> List[Type[Model]]: ...

django-stubs/db/models/base.pyi

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,15 @@ _Self = TypeVar("_Self", bound="Model")
88

99
class Model(metaclass=ModelBase):
1010
class DoesNotExist(Exception): ...
11+
class MultipleObjectsReturned(Exception): ...
1112
class Meta: ...
1213
_meta: Any
1314
_default_manager: Manager[Model]
1415
pk: Any = ...
1516
def __init__(self: _Self, *args, **kwargs) -> None: ...
1617
def delete(self, using: Any = ..., keep_parents: bool = ...) -> Tuple[int, Dict[str, int]]: ...
1718
def full_clean(self, exclude: Optional[List[str]] = ..., validate_unique: bool = ...) -> None: ...
19+
def clean(self) -> None: ...
1820
def clean_fields(self, exclude: List[str] = ...) -> None: ...
1921
def validate_unique(self, exclude: List[str] = ...) -> None: ...
2022
def save(
@@ -34,6 +36,7 @@ class Model(metaclass=ModelBase):
3436
): ...
3537
def refresh_from_db(self: _Self, using: Optional[str] = ..., fields: Optional[List[str]] = ...) -> _Self: ...
3638
def get_deferred_fields(self) -> Set[str]: ...
39+
def __getstate__(self) -> dict: ...
3740

3841
class ModelStateFieldsCacheDescriptor: ...
3942

mypy_django_plugin/transformers/models.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
from mypy.nodes import (
66
ARG_STAR, ARG_STAR2, MDEF, Argument, CallExpr, ClassDef, Expression, IndexExpr, Lvalue, MemberExpr, MypyFile,
77
NameExpr, StrExpr, SymbolTableNode, TypeInfo, Var,
8-
)
8+
ARG_POS)
99
from mypy.plugin import ClassDefContext
1010
from mypy.plugins.common import add_method
1111
from mypy.semanal import SemanticAnalyzerPass2
@@ -255,10 +255,23 @@ def add_dummy_init_method(ctx: ClassDefContext) -> None:
255255
type_annotation=any, initializer=None, kind=ARG_STAR2)
256256

257257
add_method(ctx, '__init__', [pos_arg, kw_arg], NoneTyp())
258+
258259
# mark as model class
259260
ctx.cls.info.metadata.setdefault('django', {})['generated_init'] = True
260261

261262

263+
def add_get_set_attr_fallback_to_any(ctx: ClassDefContext):
264+
any = AnyType(TypeOfAny.special_form)
265+
266+
name_arg = Argument(variable=Var('name', any),
267+
type_annotation=any, initializer=None, kind=ARG_POS)
268+
add_method(ctx, '__getattr__', [name_arg], any)
269+
270+
value_arg = Argument(variable=Var('value', any),
271+
type_annotation=any, initializer=None, kind=ARG_POS)
272+
add_method(ctx, '__setattr__', [name_arg, value_arg], any)
273+
274+
262275
def process_model_class(ctx: ClassDefContext) -> None:
263276
initializers = [
264277
InjectAnyAsBaseForNestedMeta,
@@ -273,4 +286,4 @@ def process_model_class(ctx: ClassDefContext) -> None:
273286
add_dummy_init_method(ctx)
274287

275288
# allow unspecified attributes for now
276-
ctx.cls.info.fallback_to_any = True
289+
add_get_set_attr_fallback_to_any(ctx)

scripts/typecheck_tests.py

Lines changed: 34 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,12 @@
5959
'Argument 1 to "loads" has incompatible type "Union[bytes, str, None]"; '
6060
+ 'expected "Union[str, bytes, bytearray]"',
6161
'Incompatible types in assignment (expression has type "None", variable has type Module)',
62-
'note:'
62+
'note:',
63+
# Suppress false-positive error due to mypy being overly strict with base class compatibility checks even though
64+
# objects/_default_manager are redefined in the subclass to be compatible with the base class definition.
65+
# Can be removed when mypy issue is fixed: https://github.com/python/mypy/issues/2619
66+
re.compile(r'Definition of "(objects|_default_manager)" in base class "[A-Za-z0-9]+" is incompatible with '
67+
r'definition in base class "[A-Za-z0-9]+"'),
6368
],
6469
'admin_changelist': [
6570
'Incompatible types in assignment (expression has type "FilteredChildAdmin", variable has type "ChildAdmin")'
@@ -213,11 +218,12 @@
213218
'Unexpected keyword argument "headline__startswith" for "in_bulk" of "QuerySet"',
214219
],
215220
'many_to_one': [
216-
'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")'
221+
'Incompatible type for "parent" of "Child" (got "None", expected "Union[Parent, Combinable]")',
222+
'Incompatible type for "parent" of "Child" (got "Child", expected "Union[Parent, Combinable]")'
217223
],
218224
'model_meta': [
219225
'"object" has no attribute "items"',
220-
'"Field" has no attribute "many_to_many"'
226+
'"Field" has no attribute "many_to_many"',
221227
],
222228
'model_forms': [
223229
'Argument "instance" to "InvalidModelForm" has incompatible type "Type[Category]"; expected "Optional[Model]"',
@@ -227,8 +233,14 @@
227233
'Incompatible types in assignment (expression has type "Type[Person]", variable has type',
228234
'Unexpected keyword argument "name" for "Person"',
229235
'Cannot assign multiple types to name "PersonTwoImages" without an explicit "Type[...]" annotation',
230-
'Incompatible types in assignment (expression has type "Type[Person]", '
231-
+ 'base class "ImageFieldTestMixin" defined the type as "Type[PersonWithHeightAndWidth]")',
236+
re.compile(
237+
r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class "IntegerFieldTests"'
238+
r' defined the type as "Type\[IntegerModel\]"\)'),
239+
re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class'
240+
r' "ImageFieldTestMixin" defined the type as "Type\[PersonWithHeightAndWidth\]"\)'),
241+
'Incompatible import of "Person"',
242+
'Incompatible types in assignment (expression has type "FloatModel", variable has type '
243+
'"Union[float, int, str, Combinable]")',
232244
],
233245
'model_formsets': [
234246
'Incompatible types in string interpolation (expression has type "object", '
@@ -261,7 +273,7 @@
261273
'Argument 1 to "RunPython" has incompatible type "str"; expected "Callable[..., Any]"',
262274
'FakeLoader',
263275
'Argument 1 to "append" of "list" has incompatible type "AddIndex"; expected "CreateModel"',
264-
'Unsupported operand types for - ("Set[Any]" and "None")'
276+
'Unsupported operand types for - ("Set[Any]" and "None")',
265277
],
266278
'middleware_exceptions': [
267279
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any]"; expected "str"'
@@ -282,7 +294,11 @@
282294
+ 'expected "Optional[Type[JSONEncoder]]"',
283295
'for model "CITestModel"',
284296
'Incompatible type for "field" of "IntegerArrayModel" (got "None", '
285-
+ 'expected "Union[Sequence[int], Combinable]")'
297+
+ 'expected "Union[Sequence[int], Combinable]")',
298+
re.compile(r'Incompatible types in assignment \(expression has type "Type\[.+?\]", base class "UnaccentTest" '
299+
r'defined the type as "Type\[CharFieldModel\]"\)'),
300+
'Incompatible types in assignment (expression has type "Type[TextFieldModel]", base class "TrigramTest" '
301+
'defined the type as "Type[CharFieldModel]")',
286302
],
287303
'properties': [
288304
re.compile('Unexpected attribute "(full_name|full_name_2)" for model "Person"')
@@ -291,6 +307,12 @@
291307
'Incompatible types in assignment (expression has type "None", variable has type "str")',
292308
'Invalid index type "Optional[str]" for "Dict[str, int]"; expected type "str"',
293309
'No overload variant of "values_list" of "QuerySet" matches argument types "str", "bool", "bool"',
310+
'Unsupported operand types for & ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")',
311+
'Unsupported operand types for | ("QuerySet[Author, Author]" and "QuerySet[Tag, Tag]")',
312+
'Incompatible types in assignment (expression has type "ObjectB", variable has type "ObjectA")',
313+
'Incompatible types in assignment (expression has type "ObjectC", variable has type "ObjectA")',
314+
'Incompatible type for "objectb" of "ObjectC" (got "ObjectA", expected'
315+
' "Union[ObjectB, Combinable, None, None]")',
294316
],
295317
'requests': [
296318
'Incompatible types in assignment (expression has type "Dict[str, str]", variable has type "QueryDict")'
@@ -303,6 +325,9 @@
303325
'"None" has no attribute "__iter__"',
304326
'has no attribute "read_by"'
305327
],
328+
'proxy_model_inheritance': [
329+
'Incompatible import of "ProxyModel"'
330+
],
306331
'signals': [
307332
'Argument 1 to "append" of "list" has incompatible type "Tuple[Any, Any, Optional[Any], Any]"; '
308333
+ 'expected "Tuple[Any, Any, Any]"'
@@ -387,7 +412,8 @@
387412
'Incompatible types in assignment (expression has type "None", variable has type "int")',
388413
],
389414
'select_related_onetoone': [
390-
'"None" has no attribute'
415+
'"None" has no attribute',
416+
'Incompatible types in assignment (expression has type "Parent2", variable has type "Parent1")',
391417
],
392418
'servers': [
393419
re.compile('Argument [0-9] to "WSGIRequestHandler"')

test-data/typecheck/managers.test

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ from django.db import models
2121
class MyModel(models.Model):
2222
authors = models.Manager[MyModel]()
2323
reveal_type(MyModel.authors) # E: Revealed type is 'django.db.models.manager.Manager[main.MyModel]'
24-
reveal_type(MyModel.objects) # E: Revealed type is 'Any'
24+
MyModel.objects # E: "Type[MyModel]" has no attribute "objects"
2525
[out]
2626

2727
[CASE test_model_objects_attribute_present_in_case_of_model_cls_passed_as_generic_parameter]

test-data/typecheck/model.test

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
[CASE test_model_subtype_relationship_and_getting_and_setting_attributes]
2+
from django.db import models
3+
4+
class A(models.Model):
5+
pass
6+
7+
class B(models.Model):
8+
b_attr = 1
9+
pass
10+
11+
class C(A):
12+
pass
13+
14+
def service(a: A) -> int:
15+
pass
16+
17+
a_instance = A()
18+
b_instance = B()
19+
reveal_type(b_instance.b_attr) # E: Revealed type is 'builtins.int'
20+
21+
22+
reveal_type(b_instance.non_existent_attribute) # E: Revealed type is 'Any'
23+
b_instance.non_existent_attribute = 2
24+
25+
service(b_instance) # E: Argument 1 to "service" has incompatible type "B"; expected "A"
26+
27+
c_instance = C()
28+
service(c_instance)

test-data/typecheck/model_create.test

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ class Parent1(models.Model):
2727
class Parent2(models.Model):
2828
id2 = models.AutoField(primary_key=True)
2929
name2 = models.CharField(max_length=50)
30-
class Child1(Parent1, Parent2):
30+
31+
# TODO: Remove the 2 expected errors on the next line once mypy issue https://github.com/python/mypy/issues/2619 is resolved:
32+
class Child1(Parent1, Parent2): # E: Definition of "objects" in base class "Parent1" is incompatible with definition in base class "Parent2" # E: Definition of "_default_manager" in base class "Parent1" is incompatible with definition in base class "Parent2"
3133
value = models.IntegerField()
3234
class Child4(Child1):
3335
value4 = models.IntegerField()

0 commit comments

Comments
 (0)