mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-08 21:14:49 +08:00
add nested Meta inheritance support for forms
This commit is contained in:
@@ -22,6 +22,7 @@ QUERYSET_CLASS_FULLNAME = 'django.db.models.query.QuerySet'
|
|||||||
BASE_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.BaseManager'
|
BASE_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.BaseManager'
|
||||||
MANAGER_CLASS_FULLNAME = 'django.db.models.manager.Manager'
|
MANAGER_CLASS_FULLNAME = 'django.db.models.manager.Manager'
|
||||||
RELATED_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.RelatedManager'
|
RELATED_MANAGER_CLASS_FULLNAME = 'django.db.models.manager.RelatedManager'
|
||||||
|
MODELFORM_CLASS_FULLNAME = 'django.forms.models.ModelForm'
|
||||||
|
|
||||||
MANAGER_CLASSES = {
|
MANAGER_CLASSES = {
|
||||||
MANAGER_CLASS_FULLNAME,
|
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:
|
def is_none_expr(expr: Expression) -> bool:
|
||||||
return isinstance(expr, NameExpr) and expr.fullname == 'builtins.None'
|
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
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import os
|
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.checker import TypeChecker
|
||||||
from mypy.nodes import MemberExpr, TypeInfo
|
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 import helpers, monkeypatch
|
||||||
from mypy_django_plugin.config import Config
|
from mypy_django_plugin.config import Config
|
||||||
from mypy_django_plugin.transformers import fields, init_create
|
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, \
|
from mypy_django_plugin.transformers.migrations import determine_model_cls_from_string_for_migrations, \
|
||||||
get_string_value_from_expr
|
get_string_value_from_expr
|
||||||
from mypy_django_plugin.transformers.models import process_model_class
|
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
|
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:
|
def determine_proper_manager_type(ctx: FunctionContext) -> Type:
|
||||||
api = cast(TypeChecker, ctx.api)
|
api = cast(TypeChecker, ctx.api)
|
||||||
ret = ctx.default_return_type
|
ret = ctx.default_return_type
|
||||||
@@ -176,22 +185,27 @@ class DjangoPlugin(Plugin):
|
|||||||
def _get_current_model_bases(self) -> Dict[str, int]:
|
def _get_current_model_bases(self) -> Dict[str, int]:
|
||||||
model_sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
|
model_sym = self.lookup_fully_qualified(helpers.MODEL_CLASS_FULLNAME)
|
||||||
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
|
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
|
||||||
if 'django' not in model_sym.node.metadata:
|
return (model_sym.node.metadata
|
||||||
model_sym.node.metadata['django'] = {
|
.setdefault('django', {})
|
||||||
'model_bases': {helpers.MODEL_CLASS_FULLNAME: 1}
|
.setdefault('model_bases', {helpers.MODEL_CLASS_FULLNAME: 1}))
|
||||||
}
|
|
||||||
return model_sym.node.metadata['django']['model_bases']
|
|
||||||
else:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def _get_current_manager_bases(self) -> Dict[str, int]:
|
def _get_current_manager_bases(self) -> Dict[str, int]:
|
||||||
manager_sym = self.lookup_fully_qualified(helpers.MANAGER_CLASS_FULLNAME)
|
model_sym = self.lookup_fully_qualified(helpers.MANAGER_CLASS_FULLNAME)
|
||||||
if manager_sym is not None and isinstance(manager_sym.node, TypeInfo):
|
if model_sym is not None and isinstance(model_sym.node, TypeInfo):
|
||||||
if 'django' not in manager_sym.node.metadata:
|
return (model_sym.node.metadata
|
||||||
manager_sym.node.metadata['django'] = {
|
.setdefault('django', {})
|
||||||
'manager_bases': {helpers.MANAGER_CLASS_FULLNAME: 1}
|
.setdefault('manager_bases', {helpers.MANAGER_CLASS_FULLNAME: 1}))
|
||||||
}
|
else:
|
||||||
return manager_sym.node.metadata['django']['manager_bases']
|
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:
|
else:
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
@@ -230,6 +244,12 @@ class DjangoPlugin(Plugin):
|
|||||||
if fullname in self._get_current_model_bases():
|
if fullname in self._get_current_model_bases():
|
||||||
return transform_model_class
|
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:
|
if fullname == helpers.DUMMY_SETTINGS_BASE_CLASS:
|
||||||
settings_modules = ['django.conf.global_settings']
|
settings_modules = ['django.conf.global_settings']
|
||||||
if self.django_settings_module:
|
if self.django_settings_module:
|
||||||
@@ -237,9 +257,6 @@ class DjangoPlugin(Plugin):
|
|||||||
return AddSettingValuesToDjangoConfObject(settings_modules,
|
return AddSettingValuesToDjangoConfObject(settings_modules,
|
||||||
self.config.ignore_missing_settings)
|
self.config.ignore_missing_settings)
|
||||||
|
|
||||||
if fullname in self._get_current_manager_bases():
|
|
||||||
return transform_manager_class
|
|
||||||
|
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def get_attribute_hook(self, fullname: str
|
def get_attribute_hook(self, fullname: str
|
||||||
|
|||||||
9
mypy_django_plugin/transformers/forms.py
Normal file
9
mypy_django_plugin/transformers/forms.py
Normal file
@@ -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
|
||||||
@@ -21,14 +21,8 @@ class ModelClassInitializer(metaclass=ABCMeta):
|
|||||||
def from_ctx(cls, ctx: ClassDefContext):
|
def from_ctx(cls, ctx: ClassDefContext):
|
||||||
return cls(api=cast(SemanticAnalyzerPass2, ctx.api), model_classdef=ctx.cls)
|
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]:
|
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:
|
if meta_node is None:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
@@ -80,7 +74,7 @@ class SetIdAttrsForRelatedFields(ModelClassInitializer):
|
|||||||
|
|
||||||
class InjectAnyAsBaseForNestedMeta(ModelClassInitializer):
|
class InjectAnyAsBaseForNestedMeta(ModelClassInitializer):
|
||||||
def run(self) -> None:
|
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:
|
if meta_node is None:
|
||||||
return None
|
return None
|
||||||
meta_node.fallback_to_any = True
|
meta_node.fallback_to_any = True
|
||||||
|
|||||||
19
test-data/typecheck/forms.test
Normal file
19
test-data/typecheck/forms.test
Normal file
@@ -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]
|
||||||
Reference in New Issue
Block a user