Skip to content

Commit 2c23d8e

Browse files
authored
Use runtime information to determine whether class is a models.Model subclass (#182)
1 parent 5910bd1 commit 2c23d8e

File tree

6 files changed

+26
-25
lines changed

6 files changed

+26
-25
lines changed

mypy_django_plugin/django/context.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
)
77

88
from django.core.exceptions import FieldError
9+
from django.db import models
910
from django.db.models.base import Model
1011
from django.db.models.fields import AutoField, CharField, Field
1112
from django.db.models.fields.related import ForeignKey, RelatedField
@@ -285,3 +286,15 @@ def get_expected_types(self, api: TypeChecker, model_cls: Type[Model], *, method
285286
expected_types[field_name] = gfk_set_type
286287

287288
return expected_types
289+
290+
@cached_property
291+
def model_base_classes(self) -> Set[str]:
292+
model_classes = self.apps_registry.get_models()
293+
294+
all_model_bases = set()
295+
for model_cls in model_classes:
296+
for base_cls in model_cls.mro():
297+
if issubclass(base_cls, models.Model):
298+
all_model_bases.add(helpers.get_class_fullname(base_cls))
299+
300+
return all_model_bases

mypy_django_plugin/lib/helpers.py

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -255,8 +255,6 @@ def get_typechecker_api(ctx: Union[AttributeContext, MethodContext, FunctionCont
255255
return cast(TypeChecker, ctx.api)
256256

257257

258-
def get_all_model_mixins(api: TypeChecker) -> Set[str]:
259-
basemodel_info = lookup_fully_qualified_typeinfo(api, fullnames.MODEL_CLASS_FULLNAME)
260-
if basemodel_info is None:
261-
return set()
262-
return set(get_django_metadata(basemodel_info).get('model_mixins', dict).keys())
258+
def is_model_subclass_info(info: TypeInfo, django_context: 'DjangoContext') -> bool:
259+
return (info.fullname() in django_context.model_base_classes
260+
or info.has_base(fullnames.MODEL_CLASS_FULLNAME))

mypy_django_plugin/main.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ def get_function_hook(self, fullname: str
174174
if info.has_base(fullnames.FIELD_FULLNAME):
175175
return partial(fields.transform_into_proper_return_type, django_context=self.django_context)
176176

177-
if info.has_base(fullnames.MODEL_CLASS_FULLNAME):
177+
if helpers.is_model_subclass_info(info, self.django_context):
178178
return partial(init_create.redefine_and_typecheck_model_init, django_context=self.django_context)
179179
return None
180180

@@ -213,7 +213,8 @@ def get_method_hook(self, fullname: str
213213

214214
def get_base_class_hook(self, fullname: str
215215
) -> Optional[Callable[[ClassDefContext], None]]:
216-
if fullname in self._get_current_model_bases():
216+
if (fullname in self.django_context.model_base_classes
217+
or fullname in self._get_current_model_bases()):
217218
return partial(transform_model_class, django_context=self.django_context)
218219

219220
if fullname in self._get_current_manager_bases():

mypy_django_plugin/transformers/fields.py

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
def _get_current_field_from_assignment(ctx: FunctionContext, django_context: DjangoContext) -> Optional[Field]:
1616
outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class()
1717
if (outer_model_info is None
18-
or not outer_model_info.has_base(fullnames.MODEL_CLASS_FULLNAME)):
18+
or not helpers.is_model_subclass_info(outer_model_info, django_context)):
1919
return None
2020

2121
field_name = None
@@ -117,10 +117,9 @@ def transform_into_proper_return_type(ctx: FunctionContext, django_context: Djan
117117

118118
outer_model_info = helpers.get_typechecker_api(ctx).scope.active_class()
119119
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))):
122-
# not inside models.Model class
120+
or not helpers.is_model_subclass_info(outer_model_info, django_context)):
123121
return ctx.default_return_type
122+
124123
assert isinstance(outer_model_info, TypeInfo)
125124

126125
if helpers.has_any_of_bases(default_return_type.type, fullnames.RELATED_FIELDS_CLASSES):

mypy_django_plugin/transformers/models.py

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -218,18 +218,6 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
218218
]))
219219

220220

221-
class RecordAllModelMixins(ModelClassInitializer):
222-
def run(self) -> None:
223-
basemodel_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.MODEL_CLASS_FULLNAME)
224-
basemodel_metadata = helpers.get_django_metadata(basemodel_info)
225-
if 'model_mixins' not in basemodel_metadata:
226-
basemodel_metadata['model_mixins'] = {}
227-
228-
for base_info in self.model_classdef.info.mro[1:]:
229-
if base_info.fullname() != 'builtins.object':
230-
basemodel_metadata['model_mixins'][base_info.fullname()] = 1
231-
232-
233221
def process_model_class(ctx: ClassDefContext,
234222
django_context: DjangoContext) -> None:
235223
initializers = [
@@ -241,7 +229,6 @@ def process_model_class(ctx: ClassDefContext,
241229
AddRelatedManagers,
242230
AddExtraFieldMethods,
243231
AddMetaOptionsAttribute,
244-
RecordAllModelMixins,
245232
]
246233
for initializer_cls in initializers:
247234
try:

test-data/typecheck/fields/test_base.yml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -144,7 +144,10 @@
144144
- path: myapp/models.py
145145
content: |
146146
from django.db import models
147-
class AuthMixin:
147+
class AuthMixin(models.Model):
148+
class Meta:
149+
abstract = True
148150
username = models.CharField(max_length=100)
151+
149152
class MyModel(AuthMixin, models.Model):
150153
pass

0 commit comments

Comments
 (0)