Skip to content

Commit 23ad650

Browse files
authored
add custom Field processing for mixins used in the Model subclasses (#167)
1 parent 64720f4 commit 23ad650

File tree

4 files changed

+42
-2
lines changed

4 files changed

+42
-2
lines changed

mypy_django_plugin/lib/helpers.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@
1111
)
1212
from mypy.types import AnyType, Instance, NoneTyp, TupleType, Type as MypyType, TypeOfAny, TypedDictType, UnionType
1313

14+
from mypy_django_plugin.lib import fullnames
15+
1416
if TYPE_CHECKING:
1517
from mypy_django_plugin.django.context import DjangoContext
1618

@@ -247,3 +249,10 @@ def get_typechecker_api(ctx: Union[AttributeContext, MethodContext, FunctionCont
247249
if not isinstance(ctx.api, TypeChecker):
248250
raise ValueError('Not a TypeChecker')
249251
return cast(TypeChecker, ctx.api)
252+
253+
254+
def get_all_model_mixins(api: TypeChecker) -> Set[str]:
255+
basemodel_info = lookup_fully_qualified_typeinfo(api, fullnames.MODEL_CLASS_FULLNAME)
256+
if basemodel_info is None:
257+
return set()
258+
return set(get_django_metadata(basemodel_info).get('model_mixins', dict).keys())

mypy_django_plugin/transformers/fields.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,9 @@ def transform_into_proper_return_type(ctx: FunctionContext, django_context: Djan
116116
assert isinstance(default_return_type, Instance)
117117

118118
outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class()
119-
if not outer_model_info or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME):
119+
if (outer_model_info is None
120+
or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME)
121+
and outer_model_info.fullname() not in helpers.get_all_model_mixins(helpers.get_typechecker_api(ctx))):
120122
# not inside models.Model class
121123
return ctx.default_return_type
122124
assert isinstance(outer_model_info, TypeInfo)

mypy_django_plugin/transformers/models.py

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,18 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
212212
]))
213213

214214

215+
class RecordAllModelMixins(ModelClassInitializer):
216+
def run(self) -> None:
217+
basemodel_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.MODEL_CLASS_FULLNAME)
218+
basemodel_metadata = helpers.get_django_metadata(basemodel_info)
219+
if 'model_mixins' not in basemodel_metadata:
220+
basemodel_metadata['model_mixins'] = {}
221+
222+
for base_info in self.model_classdef.info.mro[1:]:
223+
if base_info.fullname() != 'builtins.object':
224+
basemodel_metadata['model_mixins'][base_info.fullname()] = 1
225+
226+
215227
def process_model_class(ctx: ClassDefContext,
216228
django_context: DjangoContext) -> None:
217229
initializers = [
@@ -220,7 +232,8 @@ def process_model_class(ctx: ClassDefContext,
220232
AddRelatedModelsId,
221233
AddManagers,
222234
AddExtraFieldMethods,
223-
AddMetaOptionsAttribute
235+
AddMetaOptionsAttribute,
236+
RecordAllModelMixins,
224237
]
225238
for initializer_cls in initializers:
226239
try:

test-data/typecheck/fields/test_base.yml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -132,3 +132,19 @@
132132
myfield: models.IntegerField[int, int]
133133
reveal_type(MyClass.myfield) # N: Revealed type is 'django.db.models.fields.IntegerField[builtins.int, builtins.int]'
134134
reveal_type(MyClass().myfield) # N: Revealed type is 'django.db.models.fields.IntegerField[builtins.int, builtins.int]'
135+
136+
- case: fields_inside_mixins_used_in_model_subclasses_resolved_as_primitives
137+
main: |
138+
from myapp.models import MyModel, AuthMixin
139+
reveal_type(MyModel().username) # N: Revealed type is 'builtins.str*'
140+
installed_apps:
141+
- myapp
142+
files:
143+
- path: myapp/__init__.py
144+
- path: myapp/models.py
145+
content: |
146+
from django.db import models
147+
class AuthMixin:
148+
username = models.CharField(max_length=100)
149+
class MyModel(AuthMixin, models.Model):
150+
pass

0 commit comments

Comments
 (0)