From 14ea848dd7de9cf5df506e2a4425ed067df507f7 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov Date: Wed, 20 Feb 2019 21:52:28 +0300 Subject: [PATCH] add nested Meta inheritance support for forms --- mypy_django_plugin/helpers.py | 8 ++++ mypy_django_plugin/main.py | 49 +++++++++++++++-------- mypy_django_plugin/transformers/forms.py | 9 +++++ mypy_django_plugin/transformers/models.py | 10 +---- test-data/typecheck/forms.test | 19 +++++++++ 5 files changed, 71 insertions(+), 24 deletions(-) create mode 100644 mypy_django_plugin/transformers/forms.py create mode 100644 test-data/typecheck/forms.test diff --git a/mypy_django_plugin/helpers.py b/mypy_django_plugin/helpers.py index e42814d..93a2482 100644 --- a/mypy_django_plugin/helpers.py +++ b/mypy_django_plugin/helpers.py @@ -22,6 +22,7 @@ QUERYSET_CLASS_FULLNAME = 'django.db.models.query.QuerySet' BASE_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.BaseManager' MANAGER_CLASS_FULLNAME = 'django.db.models.manager.Manager' RELATED_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.RelatedManager' +MODELFORM_CLASS_FULLNAME = 'django.forms.models.ModelForm' MANAGER_CLASSES = { MANAGER_CLASS_FULLNAME, @@ -273,3 +274,10 @@ def has_any_of_bases(info: TypeInfo, bases: typing.Sequence[str]) -> bool: def is_none_expr(expr: Expression) -> bool: return isinstance(expr, NameExpr) and expr.fullname == 'builtins.None' + + +def get_nested_meta_node_for_current_class(info: TypeInfo) -> Optional[TypeInfo]: + metaclass_sym = info.names.get('Meta') + if metaclass_sym is not None and isinstance(metaclass_sym.node, TypeInfo): + return metaclass_sym.node + return None diff --git a/mypy_django_plugin/main.py b/mypy_django_plugin/main.py index a2c87a6..b7e26e5 100644 --- a/mypy_django_plugin/main.py +++ b/mypy_django_plugin/main.py @@ -1,5 +1,5 @@ import os -from typing import Callable, Dict, Optional, Union, cast +from typing import Callable, Optional, Set, Union, cast, Dict from mypy.checker import TypeChecker from mypy.nodes import MemberExpr, TypeInfo @@ -9,6 +9,7 @@ from mypy.types import AnyType, Instance, Type, TypeOfAny, TypeType, UnionType from mypy_django_plugin import helpers, monkeypatch from mypy_django_plugin.config import Config from mypy_django_plugin.transformers import fields, init_create +from mypy_django_plugin.transformers.forms import make_meta_nested_class_inherit_from_any from mypy_django_plugin.transformers.migrations import determine_model_cls_from_string_for_migrations, \ get_string_value_from_expr from mypy_django_plugin.transformers.models import process_model_class @@ -33,6 +34,14 @@ def transform_manager_class(ctx: ClassDefContext) -> None: sym.node.metadata['django']['manager_bases'][ctx.cls.fullname] = 1 +def transform_modelform_class(ctx: ClassDefContext) -> None: + sym = ctx.api.lookup_fully_qualified_or_none(helpers.MODELFORM_CLASS_FULLNAME) + if sym is not None and isinstance(sym.node, TypeInfo): + sym.node.metadata['django']['modelform_bases'][ctx.cls.fullname] = 1 + + make_meta_nested_class_inherit_from_any(ctx) + + def determine_proper_manager_type(ctx: FunctionContext) -> Type: api = cast(TypeChecker, ctx.api) ret = ctx.default_return_type @@ -176,22 +185,27 @@ class DjangoPlugin(Plugin): def _get_current_model_bases(self) -> Dict[str, int]: model_sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME) if model_sym is not None and isinstance(model_sym.node, TypeInfo): - if 'django' not in model_sym.node.metadata: - model_sym.node.metadata['django'] = { - 'model_bases': {helpers.MODEL_CLASS_FULLNAME: 1} - } - return model_sym.node.metadata['django']['model_bases'] + return (model_sym.node.metadata + .setdefault('django', {}) + .setdefault('model_bases', {helpers.MODEL_CLASS_FULLNAME: 1})) else: return {} def _get_current_manager_bases(self) -> Dict[str, int]: - manager_sym = self.lookup_fully_qualified(helpers.MANAGER_CLASS_FULLNAME) - if manager_sym is not None and isinstance(manager_sym.node, TypeInfo): - if 'django' not in manager_sym.node.metadata: - manager_sym.node.metadata['django'] = { - 'manager_bases': {helpers.MANAGER_CLASS_FULLNAME: 1} - } - return manager_sym.node.metadata['django']['manager_bases'] + model_sym = self.lookup_fully_qualified(helpers.MANAGER_CLASS_FULLNAME) + if model_sym is not None and isinstance(model_sym.node, TypeInfo): + return (model_sym.node.metadata + .setdefault('django', {}) + .setdefault('manager_bases', {helpers.MANAGER_CLASS_FULLNAME: 1})) + else: + return {} + + def _get_current_modelform_bases(self) -> Dict[str, int]: + model_sym = self.lookup_fully_qualified(helpers.MODELFORM_CLASS_FULLNAME) + if model_sym is not None and isinstance(model_sym.node, TypeInfo): + return (model_sym.node.metadata + .setdefault('django', {}) + .setdefault('modelform_bases', {helpers.MODELFORM_CLASS_FULLNAME: 1})) else: return {} @@ -230,6 +244,12 @@ class DjangoPlugin(Plugin): if fullname in self._get_current_model_bases(): return transform_model_class + if fullname in self._get_current_manager_bases(): + return transform_manager_class + + if fullname in self._get_current_modelform_bases(): + return transform_modelform_class + if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS: settings_modules = ['django.conf.global_settings'] if self.django_settings_module: @@ -237,9 +257,6 @@ class DjangoPlugin(Plugin): return AddSettingValuesToDjangoConfObject(settings_modules, self.config.ignore_missing_settings) - if fullname in self._get_current_manager_bases(): - return transform_manager_class - return None def get_attribute_hook(self, fullname: str diff --git a/mypy_django_plugin/transformers/forms.py b/mypy_django_plugin/transformers/forms.py new file mode 100644 index 0000000..535c0e8 --- /dev/null +++ b/mypy_django_plugin/transformers/forms.py @@ -0,0 +1,9 @@ +from mypy.plugin import ClassDefContext +from mypy_django_plugin import helpers + + +def make_meta_nested_class_inherit_from_any(ctx: ClassDefContext) -> None: + meta_node = helpers.get_nested_meta_node_for_current_class(ctx.cls.info) + if meta_node is None: + return None + meta_node.fallback_to_any = True diff --git a/mypy_django_plugin/transformers/models.py b/mypy_django_plugin/transformers/models.py index b56e3f8..ffebd07 100644 --- a/mypy_django_plugin/transformers/models.py +++ b/mypy_django_plugin/transformers/models.py @@ -21,14 +21,8 @@ class ModelClassInitializer(metaclass=ABCMeta): def from_ctx(cls, ctx: ClassDefContext): return cls(api=cast(SemanticAnalyzerPass2, ctx.api), model_classdef=ctx.cls) - def get_nested_meta_node(self) -> Optional[TypeInfo]: - metaclass_sym = self.model_classdef.info.names.get('Meta') - if metaclass_sym is not None and isinstance(metaclass_sym.node, TypeInfo): - return metaclass_sym.node - return None - def get_meta_attribute(self, name: str) -> Optional[Expression]: - meta_node = self.get_nested_meta_node() + meta_node = helpers.get_nested_meta_node_for_current_class(self.model_classdef.info) if meta_node is None: return None @@ -80,7 +74,7 @@ class SetIdAttrsForRelatedFields(ModelClassInitializer): class InjectAnyAsBaseForNestedMeta(ModelClassInitializer): def run(self) -> None: - meta_node = self.get_nested_meta_node() + meta_node = helpers.get_nested_meta_node_for_current_class(self.model_classdef.info) if meta_node is None: return None meta_node.fallback_to_any = True diff --git a/test-data/typecheck/forms.test b/test-data/typecheck/forms.test new file mode 100644 index 0000000..c00239e --- /dev/null +++ b/test-data/typecheck/forms.test @@ -0,0 +1,19 @@ +[CASE no_incompatible_meta_nested_class_false_positive] +from django.db import models +from django import forms + +class Article(models.Model): + pass +class Category(models.Model): + pass +class ArticleForm(forms.ModelForm): + class Meta: + model = Article + fields = '__all__' +class CategoryForm(forms.ModelForm): + class Meta: + model = Category + fields = '__all__' +class CompositeForm(ArticleForm, CategoryForm): + pass +[out] \ No newline at end of file