Skip to content

Commit

Permalink
Fix crash if model from same app referenced in RelatedField cannot be…
Browse files Browse the repository at this point in the history
… resolved (#199)

* do not crash if model from same app refd in ForeignKey cannot be resolved

* bump to 1.2.0
  • Loading branch information
mkurnikov committed Oct 5, 2019
1 parent 717be59 commit db9ff6a
Show file tree
Hide file tree
Showing 7 changed files with 69 additions and 11 deletions.
8 changes: 7 additions & 1 deletion mypy_django_plugin/django/context.py
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,10 @@ def get_expected_types(self, api: TypeChecker, model_cls: Type[Model], *, method
continue

related_model = self.get_field_related_model_cls(field)
if related_model is None:
expected_types[field_name] = AnyType(TypeOfAny.from_error)
continue

if related_model._meta.proxy_for_model is not None:
related_model = related_model._meta.proxy_for_model

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

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

def get_field_related_model_cls(self, field: Union[RelatedField, ForeignObjectRel]) -> Type[Model]:
def get_field_related_model_cls(self, field: Union[RelatedField, ForeignObjectRel]) -> Optional[Type[Model]]:
if isinstance(field, RelatedField):
related_model_cls = field.remote_field.model
else:
Expand Down
2 changes: 2 additions & 0 deletions mypy_django_plugin/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ def get_additional_deps(self, file: MypyFile) -> List[Tuple[int, str, int]]:
for field in self.django_context.get_model_fields(model_class):
if isinstance(field, RelatedField):
related_model_cls = self.django_context.get_field_related_model_cls(field)
if related_model_cls is None:
continue
related_model_module = related_model_cls.__module__
if related_model_module != file.fullname():
deps.add(self._new_dependency(related_model_module))
Expand Down
2 changes: 2 additions & 0 deletions mypy_django_plugin/transformers/fields.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ def fill_descriptor_types_for_related_field(ctx: FunctionContext, django_context
assert isinstance(current_field, RelatedField)

related_model_cls = django_context.get_field_related_model_cls(current_field)
if related_model_cls is None:
return AnyType(TypeOfAny.from_error)

related_model = related_model_cls
related_model_to_set = related_model_cls
Expand Down
26 changes: 21 additions & 5 deletions mypy_django_plugin/transformers/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,13 @@
ManyToManyRel, ManyToOneRel, OneToOneRel,
)
from mypy.nodes import (
ARG_STAR2, MDEF, Argument, SymbolTableNode, TypeInfo, Var,
ARG_STAR2, MDEF, Argument, Context, SymbolTableNode, TypeInfo, Var,
)
from mypy.plugin import ClassDefContext
from mypy.plugins import common
from mypy.types import AnyType, Instance, TypeOfAny
from mypy.types import AnyType, Instance
from mypy.types import Type as MypyType
from mypy.types import TypeOfAny

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

def create_new_var(self, name: str, typ: Instance) -> Var:
def create_new_var(self, name: str, typ: MypyType) -> Var:
# type=: type of the variable itself
var = Var(name=name, type=typ)
# var.info: type of the object variable is bound to
Expand All @@ -48,7 +50,7 @@ def create_new_var(self, name: str, typ: Instance) -> Var:
var.is_inferred = True
return var

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

Expand Down Expand Up @@ -100,6 +102,18 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
for field in model_cls._meta.get_fields():
if isinstance(field, ForeignKey):
related_model_cls = self.django_context.get_field_related_model_cls(field)
if related_model_cls is None:
error_context: Context = self.ctx.cls
field_sym = self.ctx.cls.info.get(field.name)
if field_sym is not None and field_sym.node is not None:
error_context = field_sym.node
self.api.fail(f'Cannot find model {field.related_model!r} '
f'referenced in field {field.name!r} ',
ctx=error_context)
self.add_new_node_to_model_class(field.attname,
AnyType(TypeOfAny.explicit))
continue

rel_primary_key_field = self.django_context.get_primary_key_field(related_model_cls)
field_info = self.lookup_class_typeinfo_or_incomplete_defn_error(rel_primary_key_field.__class__)
is_nullable = self.django_context.get_field_nullability(field, None)
Expand Down Expand Up @@ -163,8 +177,10 @@ def run_with_model_cls(self, model_cls: Type[Model]) -> None:
continue

related_model_cls = self.django_context.get_field_related_model_cls(relation)
related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(related_model_cls)
if related_model_cls is None:
continue

related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(related_model_cls)
if isinstance(relation, OneToOneRel):
self.add_new_node_to_model_class(attname, Instance(related_model_info, []))
continue
Expand Down
2 changes: 2 additions & 0 deletions mypy_django_plugin/transformers/querysets.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,8 @@ def get_field_type_from_lookup(ctx: MethodContext, django_context: DjangoContext

if isinstance(lookup_field, RelatedField) and lookup_field.column == lookup:
related_model_cls = django_context.get_field_related_model_cls(lookup_field)
if related_model_cls is None:
return AnyType(TypeOfAny.from_error)
lookup_field = django_context.get_primary_key_field(related_model_cls)

field_get_type = django_context.get_field_get_type(helpers.get_typechecker_api(ctx),
Expand Down
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,14 +21,14 @@ def find_stub_files(name: str) -> List[str]:
readme = f.read()

dependencies = [
'mypy>=0.730,<0.740',
'mypy>=0.730',
'typing-extensions',
'django',
]

setup(
name="django-stubs",
version="1.1.0",
version="1.2.0",
description='Mypy stubs for Django',
long_description=readme,
long_description_content_type='text/markdown',
Expand Down
36 changes: 33 additions & 3 deletions test-data/typecheck/fields/test_related.yml
Original file line number Diff line number Diff line change
Expand Up @@ -387,22 +387,52 @@
class Book2(models.Model):
publisher = models.ForeignKey(to=Publisher2, on_delete=models.CASCADE)
- case: if_model_is_defined_as_name_of_the_class_look_for_it_in_the_same_file
- case: if_model_is_defined_as_name_of_the_class_look_for_it_in_the_same_app
main: |
from myapp.models import Book
reveal_type(Book().publisher) # N: Revealed type is 'myapp.models.Publisher*'
reveal_type(Book().publisher) # N: Revealed type is 'myapp.models.publisher.Publisher*'
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models.py
- path: myapp/models/__init__.py
content: |
from .publisher import Publisher
from .book import Book
- path: myapp/models/publisher.py
content: |
from django.db import models
class Publisher(models.Model):
pass
- path: myapp/models/book.py
content: |
from django.db import models
class Book(models.Model):
publisher = models.ForeignKey(to='Publisher', on_delete=models.CASCADE)
- case: fail_if_no_model_in_the_same_app_models_init_py
main: |
from myapp.models import Book
installed_apps:
- myapp
files:
- path: myapp/__init__.py
- path: myapp/models/__init__.py
content: |
from .book import Book
- path: myapp/models/publisher.py
content: |
from django.db import models
class Publisher(models.Model):
pass
- path: myapp/models/book.py
content: |
from django.db import models
class Book(models.Model):
publisher = models.ForeignKey(to='Publisher', on_delete=models.CASCADE) # E: Cannot find model 'Publisher' referenced in field 'publisher'
- case: test_foreign_key_field_without_backwards_relation
main: |
from myapp.models import Book, Publisher
Expand Down

0 comments on commit db9ff6a

Please sign in to comment.