Skip to content

Commit db9ff6a

Browse files
authored
Fix crash if model from same app referenced in RelatedField cannot be resolved (#199)
* do not crash if model from same app refd in ForeignKey cannot be resolved * bump to 1.2.0
1 parent 717be59 commit db9ff6a

File tree

7 files changed

+69
-11
lines changed

7 files changed

+69
-11
lines changed

mypy_django_plugin/django/context.py

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,10 @@ def get_expected_types(self, api: TypeChecker, model_cls: Type[Model], *, method
177177
continue
178178

179179
related_model = self.get_field_related_model_cls(field)
180+
if related_model is None:
181+
expected_types[field_name] = AnyType(TypeOfAny.from_error)
182+
continue
183+
180184
if related_model._meta.proxy_for_model is not None:
181185
related_model = related_model._meta.proxy_for_model
182186

@@ -260,6 +264,8 @@ def get_field_get_type(self, api: TypeChecker, field: Field, *, method: str) ->
260264
is_nullable = self.get_field_nullability(field, method)
261265
if isinstance(field, RelatedField):
262266
related_model_cls = self.get_field_related_model_cls(field)
267+
if related_model_cls is None:
268+
return AnyType(TypeOfAny.from_error)
263269

264270
if method == 'values':
265271
primary_key_field = self.get_primary_key_field(related_model_cls)
@@ -274,7 +280,7 @@ def get_field_get_type(self, api: TypeChecker, field: Field, *, method: str) ->
274280
return helpers.get_private_descriptor_type(field_info, '_pyi_private_get_type',
275281
is_nullable=is_nullable)
276282

277-
def get_field_related_model_cls(self, field: Union[RelatedField, ForeignObjectRel]) -> Type[Model]:
283+
def get_field_related_model_cls(self, field: Union[RelatedField, ForeignObjectRel]) -> Optional[Type[Model]]:
278284
if isinstance(field, RelatedField):
279285
related_model_cls = field.remote_field.model
280286
else:

mypy_django_plugin/main.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,8 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
150150
for field in self.django_context.get_model_fields(model_class):
151151
if isinstance(field, RelatedField):
152152
related_model_cls = self.django_context.get_field_related_model_cls(field)
153+
if related_model_cls is None:
154+
continue
153155
related_model_module = related_model_cls.__module__
154156
if related_model_module != file.fullname():
155157
deps.add(self._new_dependency(related_model_module))

mypy_django_plugin/transformers/fields.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,8 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context
4545
assert isinstance(current_field, RelatedField)
4646

4747
related_model_cls = django_context.get_field_related_model_cls(current_field)
48+
if related_model_cls is None:
49+
return AnyType(TypeOfAny.from_error)
4850

4951
related_model = related_model_cls
5052
related_model_to_set = related_model_cls

mypy_django_plugin/transformers/models.py

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,13 @@
88
ManyToManyRel, ManyToOneRel, OneToOneRel,
99
)
1010
from mypy.nodes import (
11-
ARG_STAR2, MDEF, Argument, SymbolTableNode, TypeInfo, Var,
11+
ARG_STAR2, MDEF, Argument, Context, SymbolTableNode, TypeInfo, Var,
1212
)
1313
from mypy.plugin import ClassDefContext
1414
from mypy.plugins import common
15-
from mypy.types import AnyType, Instance, TypeOfAny
15+
from mypy.types import AnyType, Instance
16+
from mypy.types import Type as MypyType
17+
from mypy.types import TypeOfAny
1618

1719
from mypy_django_plugin.django.context import DjangoContext
1820
from mypy_django_plugin.lib import fullnames, helpers
@@ -38,7 +40,7 @@ def lookup_class_typeinfo_or_incomplete_defn_error(self, klass: type) -> TypeInf
3840
field_info = self.lookup_typeinfo_or_incomplete_defn_error(fullname)
3941
return field_info
4042

41-
def create_new_var(self, name: str, typ: Instance) -> Var:
43+
def create_new_var(self, name: str, typ: MypyType) -> Var:
4244
# type=: type of the variable itself
4345
var = Var(name=name, type=typ)
4446
# var.info: type of the object variable is bound to
@@ -48,7 +50,7 @@ def create_new_var(self, name: str, typ: Instance) -> Var:
4850
var.is_inferred = True
4951
return var
5052

51-
def add_new_node_to_model_class(self, name: str, typ: Instance) -> None:
53+
def add_new_node_to_model_class(self, name: str, typ: MypyType) -> None:
5254
var = self.create_new_var(name, typ)
5355
self.model_classdef.info.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True)
5456

@@ -100,6 +102,18 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
100102
for field in model_cls._meta.get_fields():
101103
if isinstance(field, ForeignKey):
102104
related_model_cls = self.django_context.get_field_related_model_cls(field)
105+
if related_model_cls is None:
106+
error_context: Context = self.ctx.cls
107+
field_sym = self.ctx.cls.info.get(field.name)
108+
if field_sym is not None and field_sym.node is not None:
109+
error_context = field_sym.node
110+
self.api.fail(f'Cannot find model {field.related_model!r} '
111+
f'referenced in field {field.name!r} ',
112+
ctx=error_context)
113+
self.add_new_node_to_model_class(field.attname,
114+
AnyType(TypeOfAny.explicit))
115+
continue
116+
103117
rel_primary_key_field = self.django_context.get_primary_key_field(related_model_cls)
104118
field_info = self.lookup_class_typeinfo_or_incomplete_defn_error(rel_primary_key_field.__class__)
105119
is_nullable = self.django_context.get_field_nullability(field, None)
@@ -163,8 +177,10 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
163177
continue
164178

165179
related_model_cls = self.django_context.get_field_related_model_cls(relation)
166-
related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(related_model_cls)
180+
if related_model_cls is None:
181+
continue
167182

183+
related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(related_model_cls)
168184
if isinstance(relation, OneToOneRel):
169185
self.add_new_node_to_model_class(attname, Instance(related_model_info, []))
170186
continue

mypy_django_plugin/transformers/querysets.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def get_field_type_from_lookup(ctx: MethodContext, django_context: DjangoContext
4949

5050
if isinstance(lookup_field, RelatedField) and lookup_field.column == lookup:
5151
related_model_cls = django_context.get_field_related_model_cls(lookup_field)
52+
if related_model_cls is None:
53+
return AnyType(TypeOfAny.from_error)
5254
lookup_field = django_context.get_primary_key_field(related_model_cls)
5355

5456
field_get_type = django_context.get_field_get_type(helpers.get_typechecker_api(ctx),

setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,14 +21,14 @@ def find_stub_files(name: str) -> List[str]:
2121
readme = f.read()
2222

2323
dependencies = [
24-
'mypy>=0.730,<0.740',
24+
'mypy>=0.730',
2525
'typing-extensions',
2626
'django',
2727
]
2828

2929
setup(
3030
name="django-stubs",
31-
version="1.1.0",
31+
version="1.2.0",
3232
description='Mypy stubs for Django',
3333
long_description=readme,
3434
long_description_content_type='text/markdown',

test-data/typecheck/fields/test_related.yml

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -387,22 +387,52 @@
387387
class Book2(models.Model):
388388
publisher = models.ForeignKey(to=Publisher2, on_delete=models.CASCADE)
389389
390-
- case: if_model_is_defined_as_name_of_the_class_look_for_it_in_the_same_file
390+
- case: if_model_is_defined_as_name_of_the_class_look_for_it_in_the_same_app
391391
main: |
392392
from myapp.models import Book
393-
reveal_type(Book().publisher) # N: Revealed type is 'myapp.models.Publisher*'
393+
reveal_type(Book().publisher) # N: Revealed type is 'myapp.models.publisher.Publisher*'
394394
installed_apps:
395395
- myapp
396396
files:
397397
- path: myapp/__init__.py
398-
- path: myapp/models.py
398+
- path: myapp/models/__init__.py
399+
content: |
400+
from .publisher import Publisher
401+
from .book import Book
402+
- path: myapp/models/publisher.py
399403
content: |
400404
from django.db import models
401405
class Publisher(models.Model):
402406
pass
407+
- path: myapp/models/book.py
408+
content: |
409+
from django.db import models
403410
class Book(models.Model):
404411
publisher = models.ForeignKey(to='Publisher', on_delete=models.CASCADE)
405412
413+
414+
- case: fail_if_no_model_in_the_same_app_models_init_py
415+
main: |
416+
from myapp.models import Book
417+
installed_apps:
418+
- myapp
419+
files:
420+
- path: myapp/__init__.py
421+
- path: myapp/models/__init__.py
422+
content: |
423+
from .book import Book
424+
- path: myapp/models/publisher.py
425+
content: |
426+
from django.db import models
427+
class Publisher(models.Model):
428+
pass
429+
- path: myapp/models/book.py
430+
content: |
431+
from django.db import models
432+
class Book(models.Model):
433+
publisher = models.ForeignKey(to='Publisher', on_delete=models.CASCADE) # E: Cannot find model 'Publisher' referenced in field 'publisher'
434+
435+
406436
- case: test_foreign_key_field_without_backwards_relation
407437
main: |
408438
from myapp.models import Book, Publisher

0 commit comments

Comments
 (0)