From fee4aad3d0385543ddb9a21c27211d1601655230 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Fri, 19 Jul 2019 18:30:12 +0300 Subject: [PATCH] fix OneToOneField without related_name --- django-stubs/db/models/fields/__init__.pyi | 3 ++ mypy_django_plugin/transformers/models.py | 48 +++++++++------------ test-data/typecheck/fields/test_related.yml | 17 ++++++++ 3 files changed, 41 insertions(+), 27 deletions(-) diff --git a/django-stubs/db/models/fields/__init__.pyi b/django-stubs/db/models/fields/__init__.pyi index b5eda41..ae2f7e9 100644 --- a/django-stubs/db/models/fields/__init__.pyi +++ b/django-stubs/db/models/fields/__init__.pyi @@ -116,6 +116,9 @@ class FloatField(Field[_ST, _GT]): class DecimalField(Field[_ST, _GT]): _pyi_private_set_type: Union[str, float, decimal.Decimal, Combinable] _pyi_private_get_type: decimal.Decimal + # attributes + max_digits: int = ... + decimal_places: int = ... def __init__( self, verbose_name: Optional[Union[str, bytes]] = ..., diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index 842c722..7cc4418 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -1,7 +1,8 @@ from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod -from typing import cast +from typing import cast, Type +from django.db.models.base import Model from django.db.models.fields.related import ForeignKey from django.db.models.fields.reverse_related import ManyToManyRel, ManyToOneRel, OneToOneRel from mypy.newsemanal.semanal import NewSemanticAnalyzer @@ -45,9 +46,15 @@ class ModelClassInitializer(metaclass=ABCMeta): var.is_inferred = True self.model_classdef.info.names[name] = SymbolTableNode(MDEF, var, plugin_generated=True) - @abstractmethod def run(self) -> None: - raise NotImplementedError() + model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname) + if model_cls is None: + return + self.run_with_model_cls(model_cls) + + @abstractmethod + def run_with_model_cls(self, model_cls): + pass class InjectAnyAsBaseForNestedMeta(ModelClassInitializer): @@ -63,7 +70,7 @@ class InjectAnyAsBaseForNestedMeta(ModelClassInitializer): to get around incompatible Meta inner classes for different models. """ - def run(self) -> None: + def run_with_model_cls(self, model_cls: Type[Model])-> None: meta_node = helpers.get_nested_meta_node_for_current_class(self.model_classdef.info) if meta_node is None: return None @@ -71,11 +78,7 @@ class InjectAnyAsBaseForNestedMeta(ModelClassInitializer): class AddDefaultPrimaryKey(ModelClassInitializer): - def run(self) -> None: - model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname) - if model_cls is None: - return - + def run_with_model_cls(self, model_cls: Type[Model])-> None: auto_field = model_cls._meta.auto_field if auto_field and not self.model_classdef.info.has_readable_member(auto_field.attname): # autogenerated field @@ -88,11 +91,7 @@ class AddDefaultPrimaryKey(ModelClassInitializer): class AddRelatedModelsId(ModelClassInitializer): - def run(self) -> None: - model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname) - if model_cls is None: - return - + def run_with_model_cls(self, model_cls: Type[Model])-> None: for field in model_cls._meta.get_fields(): if isinstance(field, ForeignKey): rel_primary_key_field = self.django_context.get_primary_key_field(field.related_model) @@ -104,11 +103,7 @@ class AddRelatedModelsId(ModelClassInitializer): class AddManagers(ModelClassInitializer): - def run(self): - model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname) - if model_cls is None: - return - + def run_with_model_cls(self, model_cls: Type[Model])-> None: for manager_name, manager in model_cls._meta.managers_map.items(): if manager_name not in self.model_classdef.info.names: manager_fullname = helpers.get_class_fullname(manager.__class__) @@ -126,14 +121,17 @@ class AddManagers(ModelClassInitializer): # add related managers for relation in self.django_context.get_model_relations(model_cls): - attname = relation.related_name + attname = relation.get_accessor_name() if attname is None: - attname = relation.name + '_set' + # no reverse accessor + continue related_model_info = self.lookup_class_typeinfo_or_incomplete_defn_error(relation.related_model) + if isinstance(relation, OneToOneRel): self.add_new_node_to_model_class(attname, Instance(related_model_info, [])) continue + if isinstance(relation, (ManyToOneRel, ManyToManyRel)): manager_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.RELATED_MANAGER_CLASS_FULLNAME) self.add_new_node_to_model_class(attname, @@ -142,11 +140,7 @@ class AddManagers(ModelClassInitializer): class AddExtraFieldMethods(ModelClassInitializer): - def run(self): - model_cls = self.django_context.get_model_class_by_fullname(self.model_classdef.fullname) - if model_cls is None: - return - + def run_with_model_cls(self, model_cls: Type[Model])-> None: # get_FOO_display for choices for field in self.django_context.get_model_fields(model_cls): if field.choices: @@ -178,7 +172,7 @@ class AddExtraFieldMethods(ModelClassInitializer): class AddMetaOptionsAttribute(ModelClassInitializer): - def run(self): + def run_with_model_cls(self, model_cls: Type[Model])-> None: if '_meta' not in self.model_classdef.info.names: options_info = self.lookup_typeinfo_or_incomplete_defn_error(fullnames.OPTIONS_CLASS_FULLNAME) self.add_new_node_to_model_class('_meta', diff --git a/test-data/typecheck/fields/test_related.yml b/test-data/typecheck/fields/test_related.yml index e3cdaa5..9c20191 100644 --- a/test-data/typecheck/fields/test_related.yml +++ b/test-data/typecheck/fields/test_related.yml @@ -84,6 +84,23 @@ class Book(models.Model): publisher = models.ForeignKey(to='myapp.Publisher', on_delete=models.CASCADE) +- case: one_to_one_field_no_related_name + main: | + from myapp.models import User, Profile + reveal_type(User().profile) # N: Revealed type is 'myapp.models.Profile' + reveal_type(Profile().user) # N: Revealed type is 'myapp.models.User*' + installed_apps: + - myapp + files: + - path: myapp/__init__.py + - path: myapp/models.py + content: | + from django.db import models + class User(models.Model): + pass + class Profile(models.Model): + user = models.OneToOneField(to=User, on_delete=models) + - case: test_circular_dependency_in_imports_with_foreign_key main: | from myapp import models