Skip to content

Commit d214db8

Browse files
Auto-inherit from TypedModelMeta
1 parent db172fa commit d214db8

File tree

4 files changed

+71
-5
lines changed

4 files changed

+71
-5
lines changed

mypy_django_plugin/lib/fullnames.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
F_EXPRESSION_FULLNAME = "django.db.models.expressions.F"
5757

5858
ANY_ATTR_ALLOWED_CLASS_FULLNAME = "django_stubs_ext.AnyAttrAllowed"
59-
59+
TYPED_MODEL_META_FULLNAME = "django_stubs_ext.db.models.TypedModelMeta"
6060
STR_PROMISE_FULLNAME = "django.utils.functional._StrPromise"
6161

6262
OBJECT_DOES_NOT_EXIST = "django.core.exceptions.ObjectDoesNotExist"

mypy_django_plugin/main.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -125,9 +125,11 @@ def get_additional_deps(self, file: MypyFile) -> list[tuple[int, str, int]]:
125125
deps.add(self._new_dependency(related_model_module))
126126

127127
return list(deps) + [
128-
# for QuerySet.annotate
128+
# For `QuerySet.annotate`
129129
self._new_dependency("django_stubs_ext"),
130-
# For Manager.from_queryset
130+
# For `TypedModelMeta` lookup in model transformers
131+
self._new_dependency("django_stubs_ext.db.models"),
132+
# For `Manager.from_queryset`
131133
self._new_dependency("django.db.models.query"),
132134
]
133135

mypy_django_plugin/transformers/models.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -262,16 +262,27 @@ class Meta:
262262
pass
263263
with
264264
class MyModel(models.Model):
265-
class Meta(Any):
265+
class Meta(TypedModelMeta):
266266
pass
267+
268+
to provide proper typing of attributes in Meta inner classes.
269+
270+
If TypedModelMeta is not available, fallback to Any as a base
267271
to get around incompatible Meta inner classes for different models.
268272
"""
269273

270274
def run(self) -> None:
271275
meta_node = helpers.get_nested_meta_node_for_current_class(self.model_classdef.info)
272276
if meta_node is None:
273277
return None
274-
meta_node.fallback_to_any = True
278+
279+
typed_model_meta_info = self.lookup_typeinfo(fullnames.TYPED_MODEL_META_FULLNAME)
280+
if typed_model_meta_info and not meta_node.has_base(fullnames.TYPED_MODEL_META_FULLNAME):
281+
# Insert TypedModelMeta just before `object` to leverage mypy's class-body semantic analysis.
282+
meta_node.mro.insert(-1, typed_model_meta_info)
283+
else:
284+
meta_node.fallback_to_any = True
285+
return None
275286

276287

277288
class AddDefaultPrimaryKey(ModelClassInitializer):

tests/typecheck/models/test_meta_options.yml

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,59 @@
7676
unique_together = {1: 2} # E: Incompatible types in assignment (expression has type "dict[int, int]", base class "TypedModelMeta" defined the type as "Sequence[Sequence[str]]") [assignment]
7777
unknown_attr = True # can't check this
7878
79+
class MyModelMultipleBaseMeta(models.Model):
80+
class Meta(MyModel.Meta, TypedModelMeta):
81+
abstract = 7 # E: Incompatible types in assignment (expression has type "int", base class "TypedModelMeta" defined the type as "bool") [assignment]
82+
83+
- case: auto_base_model_meta_incompatible_types
84+
main: |
85+
from myapp.models import MyModelWithAutoTypedMeta
86+
installed_apps:
87+
- myapp
88+
files:
89+
- path: myapp/__init__.py
90+
- path: myapp/models.py
91+
content: |
92+
from django.db import models
93+
94+
class MyModelWithAutoTypedMeta(models.Model):
95+
example = models.CharField(max_length=100)
96+
class Meta:
97+
abstract = 7 # E: Incompatible types in assignment (expression has type "int", base class "TypedModelMeta" defined the type as "bool") [assignment]
98+
verbose_name = ['test'] # E: Incompatible types in assignment (expression has type "list[str]", base class "TypedModelMeta" defined the type as "str | _StrPromise") [assignment]
99+
unique_together = {1: 2} # E: Incompatible types in assignment (expression has type "dict[int, int]", base class "TypedModelMeta" defined the type as "Sequence[Sequence[str]]") [assignment]
100+
101+
class DefinitelyNotAModel:
102+
class Meta:
103+
abstract = 7
104+
verbose_name = ['test']
105+
unique_together = {1: 2}
106+
unknown_attr = True
107+
108+
- case: auto_base_model_meta_incompatible_types_multiple_inheritance
109+
main: |
110+
from myapp.models import MyModel, MyModel2, MyModel3
111+
installed_apps:
112+
- myapp
113+
files:
114+
- path: myapp/__init__.py
115+
- path: myapp/models.py
116+
content: |
117+
from django.db import models
118+
119+
class MyModel(models.Model):
120+
class Meta:
121+
abstract = 5 # E: Incompatible types in assignment (expression has type "int", base class "TypedModelMeta" defined the type as "bool") [assignment]
122+
123+
class MyModel2(models.Model):
124+
class Meta:
125+
abstract = 5 # E: Incompatible types in assignment (expression has type "int", base class "TypedModelMeta" defined the type as "bool") [assignment]
126+
127+
class MyModel3(models.Model):
128+
class Meta(MyModel.Meta, MyModel2.Meta):
129+
abstract = 7 # Ok because both base class declare int currently
130+
verbose_name = ['test'] # E: Incompatible types in assignment (expression has type "list[str]", base class "TypedModelMeta" defined the type as "str | _StrPromise") [assignment]
131+
unique_together = {1: 2} # E: Incompatible types in assignment (expression has type "dict[int, int]", base class "TypedModelMeta" defined the type as "Sequence[Sequence[str]]") [assignment]
79132
80133
- case: instantiate_abstract_model
81134
main: |

0 commit comments

Comments
 (0)