From db9ff6aaf60dec1f85d924aae5f861e5429d7000 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Sat, 5 Oct 2019 20:00:51 +0300 Subject: [PATCH] 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 --- mypy_django_plugin/django/context.py | 8 ++++- mypy_django_plugin/main.py | 2 ++ mypy_django_plugin/transformers/fields.py | 2 ++ mypy_django_plugin/transformers/models.py | 26 +++++++++++--- mypy_django_plugin/transformers/querysets.py | 2 ++ setup.py | 4 +-- test-data/typecheck/fields/test_related.yml | 36 ++++++++++++++++++-- 7 files changed, 69 insertions(+), 11 deletions(-) diff --git a/mypy_django_plugin/django/context.py b/mypy_django_plugin/django/context.py index ff1ddd0..bca6989 100644 --- a/mypy_django_plugin/django/context.py +++ b/mypy_django_plugin/django/context.py @@ -177,6 +177,10 @@ class DjangoContext: 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 @@ -260,6 +264,8 @@ class DjangoContext: 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) @@ -274,7 +280,7 @@ class DjangoContext: 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: diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index e59e87b..424d8b0 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -150,6 +150,8 @@ class NewSemanalDjangoPlugin(Plugin): 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)) diff --git a/mypy_django_plugin/transformers/fields.py b/mypy_django_plugin/transformers/fields.py index 78b5bc5..a809cd2 100644 --- a/mypy_django_plugin/transformers/fields.py +++ b/mypy_django_plugin/transformers/fields.py @@ -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 diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 1a618a6..4e93ac4 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -8,11 +8,13 @@ from django.db.models.fields.reverse_related import ( 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 @@ -38,7 +40,7 @@ class ModelClassInitializer: 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 @@ -48,7 +50,7 @@ class ModelClassInitializer: 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) @@ -100,6 +102,18 @@ class AddRelatedModelsId(ModelClassInitializer): 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) @@ -163,8 +177,10 @@ class AddRelatedManagers(ModelClassInitializer): 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 diff --git a/mypy_django_plugin/transformers/querysets.py b/mypy_django_plugin/transformers/querysets.py index b22bf4a..8789995 100644 --- a/mypy_django_plugin/transformers/querysets.py +++ b/mypy_django_plugin/transformers/querysets.py @@ -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), diff --git a/setup.py b/setup.py index 259b81d..6524d47 100644 --- a/setup.py +++ b/setup.py @@ -21,14 +21,14 @@ with open('README.md', 'r') as f: 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', diff --git a/test-data/typecheck/fields/test_related.yml b/test-data/typecheck/fields/test_related.yml index 5f323c8..bda03c2 100644 --- a/test-data/typecheck/fields/test_related.yml +++ b/test-data/typecheck/fields/test_related.yml @@ -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