mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-07 20:54:29 +08:00
nested class Meta support
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
import typing
|
import typing
|
||||||
from typing import Dict, Optional, NamedTuple
|
from typing import Dict, Optional, NamedTuple
|
||||||
|
|
||||||
from mypy.nodes import SymbolTableNode, Var, Expression
|
from mypy.nodes import SymbolTableNode, Var, Expression, StrExpr, MypyFile, TypeInfo
|
||||||
from mypy.plugin import FunctionContext
|
from mypy.plugin import FunctionContext
|
||||||
from mypy.types import Type, Instance, UnionType, NoneTyp
|
from mypy.types import Type, Instance, UnionType, NoneTyp
|
||||||
|
|
||||||
@@ -67,3 +67,23 @@ def make_required(typ: Type) -> Type:
|
|||||||
|
|
||||||
def get_obj_type_name(typ: typing.Type) -> str:
|
def get_obj_type_name(typ: typing.Type) -> str:
|
||||||
return typ.__module__ + '.' + typ.__qualname__
|
return typ.__module__ + '.' + typ.__qualname__
|
||||||
|
|
||||||
|
|
||||||
|
def get_models_file(app_name: str, all_modules: typing.Dict[str, MypyFile]) -> Optional[MypyFile]:
|
||||||
|
models_module = '.'.join([app_name, 'models'])
|
||||||
|
return all_modules.get(models_module)
|
||||||
|
|
||||||
|
|
||||||
|
def get_model_type_from_string(expr: StrExpr,
|
||||||
|
all_modules: Dict[str, MypyFile]) -> Optional[TypeInfo]:
|
||||||
|
app_name, model_name = expr.value.split('.')
|
||||||
|
|
||||||
|
models_file = get_models_file(app_name, all_modules)
|
||||||
|
if models_file is None:
|
||||||
|
# not imported so far, not supported
|
||||||
|
return None
|
||||||
|
sym = models_file.names.get(model_name)
|
||||||
|
if not sym or not isinstance(sym.node, TypeInfo):
|
||||||
|
# no such model found in the app / node is not a class definition
|
||||||
|
return None
|
||||||
|
return sym.node
|
||||||
|
|||||||
@@ -1,27 +1,81 @@
|
|||||||
import os
|
import os
|
||||||
from typing import Callable, Optional
|
from typing import Callable, Optional, cast
|
||||||
|
|
||||||
|
from mypy.nodes import AssignmentStmt, CallExpr, MemberExpr, StrExpr, NameExpr
|
||||||
from mypy.options import Options
|
from mypy.options import Options
|
||||||
from mypy.plugin import Plugin, FunctionContext, ClassDefContext
|
from mypy.plugin import Plugin, FunctionContext, ClassDefContext
|
||||||
from mypy.types import Type
|
from mypy.semanal import SemanticAnalyzerPass2
|
||||||
|
from mypy.types import Type, Instance
|
||||||
|
|
||||||
from mypy_django_plugin import helpers, monkeypatch
|
from mypy_django_plugin import helpers, monkeypatch
|
||||||
|
from mypy_django_plugin.plugins.meta_inner_class import inject_any_as_base_for_nested_class_meta
|
||||||
from mypy_django_plugin.plugins.objects_queryset import set_objects_queryset_to_model_class
|
from mypy_django_plugin.plugins.objects_queryset import set_objects_queryset_to_model_class
|
||||||
from mypy_django_plugin.plugins.postgres_fields import determine_type_of_array_field
|
from mypy_django_plugin.plugins.fields import determine_type_of_array_field, \
|
||||||
from mypy_django_plugin.plugins.related_fields import OneToOneFieldHook, \
|
add_int_id_attribute_if_primary_key_true_is_not_present
|
||||||
ForeignKeyHook, set_fieldname_attrs_for_related_fields
|
from mypy_django_plugin.plugins.related_fields import set_fieldname_attrs_for_related_fields, add_new_var_node_to_class, \
|
||||||
|
extract_to_parameter_as_get_ret_type
|
||||||
from mypy_django_plugin.plugins.setup_settings import DjangoConfSettingsInitializerHook
|
from mypy_django_plugin.plugins.setup_settings import DjangoConfSettingsInitializerHook
|
||||||
|
|
||||||
|
|
||||||
base_model_classes = {helpers.MODEL_CLASS_FULLNAME}
|
base_model_classes = {helpers.MODEL_CLASS_FULLNAME}
|
||||||
|
|
||||||
|
|
||||||
|
def add_related_managers_from_referred_foreign_keys_to_model(ctx: ClassDefContext) -> None:
|
||||||
|
api = cast(SemanticAnalyzerPass2, ctx.api)
|
||||||
|
for stmt in ctx.cls.defs.body:
|
||||||
|
if not isinstance(stmt, AssignmentStmt):
|
||||||
|
continue
|
||||||
|
if len(stmt.lvalues) > 1:
|
||||||
|
# not supported yet
|
||||||
|
continue
|
||||||
|
rvalue = stmt.rvalue
|
||||||
|
if not isinstance(rvalue, CallExpr):
|
||||||
|
continue
|
||||||
|
if (not isinstance(rvalue.callee, MemberExpr)
|
||||||
|
or not rvalue.callee.fullname in {helpers.FOREIGN_KEY_FULLNAME,
|
||||||
|
helpers.ONETOONE_FIELD_FULLNAME}):
|
||||||
|
continue
|
||||||
|
if 'related_name' not in rvalue.arg_names:
|
||||||
|
# positional related_name is not supported yet
|
||||||
|
continue
|
||||||
|
related_name = rvalue.args[rvalue.arg_names.index('related_name')].value
|
||||||
|
|
||||||
|
if 'to' in rvalue.arg_names:
|
||||||
|
expr = rvalue.args[rvalue.arg_names.index('to')]
|
||||||
|
else:
|
||||||
|
# first positional argument
|
||||||
|
expr = rvalue.args[0]
|
||||||
|
|
||||||
|
if isinstance(expr, StrExpr):
|
||||||
|
model_typeinfo = helpers.get_model_type_from_string(expr,
|
||||||
|
all_modules=api.modules)
|
||||||
|
if model_typeinfo is None:
|
||||||
|
continue
|
||||||
|
elif isinstance(expr, NameExpr):
|
||||||
|
model_typeinfo = expr.node
|
||||||
|
else:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if rvalue.callee.fullname == helpers.FOREIGN_KEY_FULLNAME:
|
||||||
|
typ = api.named_type_or_none(helpers.QUERYSET_CLASS_FULLNAME,
|
||||||
|
args=[Instance(ctx.cls.info, [])])
|
||||||
|
else:
|
||||||
|
typ = Instance(ctx.cls.info, [])
|
||||||
|
|
||||||
|
if typ is None:
|
||||||
|
continue
|
||||||
|
add_new_var_node_to_class(model_typeinfo, related_name, typ)
|
||||||
|
|
||||||
|
|
||||||
class TransformModelClassHook(object):
|
class TransformModelClassHook(object):
|
||||||
def __call__(self, ctx: ClassDefContext) -> None:
|
def __call__(self, ctx: ClassDefContext) -> None:
|
||||||
base_model_classes.add(ctx.cls.fullname)
|
base_model_classes.add(ctx.cls.fullname)
|
||||||
|
|
||||||
set_fieldname_attrs_for_related_fields(ctx)
|
set_fieldname_attrs_for_related_fields(ctx)
|
||||||
set_objects_queryset_to_model_class(ctx)
|
set_objects_queryset_to_model_class(ctx)
|
||||||
|
inject_any_as_base_for_nested_class_meta(ctx)
|
||||||
|
add_related_managers_from_referred_foreign_keys_to_model(ctx)
|
||||||
|
add_int_id_attribute_if_primary_key_true_is_not_present(ctx)
|
||||||
|
|
||||||
|
|
||||||
class DjangoPlugin(Plugin):
|
class DjangoPlugin(Plugin):
|
||||||
@@ -29,22 +83,25 @@ class DjangoPlugin(Plugin):
|
|||||||
options: Options) -> None:
|
options: Options) -> None:
|
||||||
super().__init__(options)
|
super().__init__(options)
|
||||||
monkeypatch.replace_apply_function_plugin_method()
|
monkeypatch.replace_apply_function_plugin_method()
|
||||||
|
monkeypatch.make_inner_classes_with_inherit_from_any_compatible_with_each_other()
|
||||||
|
|
||||||
self.django_settings = os.environ.get('DJANGO_SETTINGS_MODULE')
|
self.django_settings = os.environ.get('DJANGO_SETTINGS_MODULE')
|
||||||
if self.django_settings:
|
if self.django_settings:
|
||||||
monkeypatch.load_graph_to_add_settings_file_as_a_source_seed(self.django_settings)
|
monkeypatch.load_graph_to_add_settings_file_as_a_source_seed(self.django_settings)
|
||||||
monkeypatch.inject_dependencies(self.django_settings)
|
monkeypatch.inject_dependencies(self.django_settings)
|
||||||
|
# monkeypatch.process_settings_before_dependants(self.django_settings)
|
||||||
else:
|
else:
|
||||||
monkeypatch.restore_original_load_graph()
|
monkeypatch.restore_original_load_graph()
|
||||||
monkeypatch.restore_original_dependencies_handling()
|
monkeypatch.restore_original_dependencies_handling()
|
||||||
|
|
||||||
def get_function_hook(self, fullname: str
|
def get_function_hook(self, fullname: str
|
||||||
) -> Optional[Callable[[FunctionContext], Type]]:
|
) -> Optional[Callable[[FunctionContext], Type]]:
|
||||||
if fullname == helpers.FOREIGN_KEY_FULLNAME:
|
if fullname in {helpers.FOREIGN_KEY_FULLNAME,
|
||||||
return ForeignKeyHook(settings=self.django_settings)
|
helpers.ONETOONE_FIELD_FULLNAME}:
|
||||||
|
return extract_to_parameter_as_get_ret_type
|
||||||
|
|
||||||
if fullname == helpers.ONETOONE_FIELD_FULLNAME:
|
# if fullname == helpers.ONETOONE_FIELD_FULLNAME:
|
||||||
return OneToOneFieldHook(settings=self.django_settings)
|
# return OneToOneFieldHook(settings=self.django_settings)
|
||||||
|
|
||||||
if fullname == 'django.contrib.postgres.fields.array.ArrayField':
|
if fullname == 'django.contrib.postgres.fields.array.ArrayField':
|
||||||
return determine_type_of_array_field
|
return determine_type_of_array_field
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
from .dependencies import (load_graph_to_add_settings_file_as_a_source_seed,
|
from .dependencies import (load_graph_to_add_settings_file_as_a_source_seed,
|
||||||
inject_dependencies,
|
inject_dependencies,
|
||||||
restore_original_load_graph,
|
restore_original_load_graph,
|
||||||
restore_original_dependencies_handling)
|
restore_original_dependencies_handling,
|
||||||
|
process_settings_before_dependants)
|
||||||
from .contexts import replace_apply_function_plugin_method
|
from .contexts import replace_apply_function_plugin_method
|
||||||
|
from .multiple_inheritance import make_inner_classes_with_inherit_from_any_compatible_with_each_other
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
from typing import List, Optional
|
from typing import List, Optional, AbstractSet, MutableSet, Set
|
||||||
|
|
||||||
from mypy.build import BuildManager, Graph, State
|
from mypy.build import BuildManager, Graph, State, PRI_ALL
|
||||||
from mypy.modulefinder import BuildSource
|
from mypy.modulefinder import BuildSource
|
||||||
|
|
||||||
|
|
||||||
@@ -12,6 +12,7 @@ from mypy import build
|
|||||||
|
|
||||||
old_load_graph = build.load_graph
|
old_load_graph = build.load_graph
|
||||||
OldState = build.State
|
OldState = build.State
|
||||||
|
old_sorted_components = build.sorted_components
|
||||||
|
|
||||||
|
|
||||||
def load_graph_to_add_settings_file_as_a_source_seed(settings_module: str):
|
def load_graph_to_add_settings_file_as_a_source_seed(settings_module: str):
|
||||||
@@ -50,3 +51,40 @@ def restore_original_dependencies_handling():
|
|||||||
from mypy import build
|
from mypy import build
|
||||||
|
|
||||||
build.State = OldState
|
build.State = OldState
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_dependencies(graph: Graph, state_id: str, visited_modules: Set[str]) -> Set[str]:
|
||||||
|
visited_modules.add(state_id)
|
||||||
|
dependencies = set(graph[state_id].dependencies)
|
||||||
|
for new_dep_id in dependencies.copy():
|
||||||
|
if new_dep_id not in visited_modules:
|
||||||
|
dependencies.update(_extract_dependencies(graph, new_dep_id, visited_modules))
|
||||||
|
return dependencies
|
||||||
|
|
||||||
|
|
||||||
|
def extract_module_dependencies(graph: Graph, state_id: str) -> Set[str]:
|
||||||
|
visited_modules = set()
|
||||||
|
return _extract_dependencies(graph, state_id, visited_modules=visited_modules)
|
||||||
|
|
||||||
|
|
||||||
|
def process_settings_before_dependants(settings_module: str):
|
||||||
|
def patched_sorted_components(graph: Graph,
|
||||||
|
vertices: Optional[AbstractSet[str]] = None,
|
||||||
|
pri_max: int = PRI_ALL) -> List[AbstractSet[str]]:
|
||||||
|
sccs = old_sorted_components(graph,
|
||||||
|
vertices=vertices,
|
||||||
|
pri_max=pri_max)
|
||||||
|
for i, scc in enumerate(sccs.copy()):
|
||||||
|
if 'django.conf' in scc:
|
||||||
|
django_conf_deps = set(extract_module_dependencies(graph, 'django.conf')).union({'django.conf'})
|
||||||
|
old_scc_modified = scc.difference(django_conf_deps)
|
||||||
|
new_scc = scc.difference(old_scc_modified)
|
||||||
|
if not old_scc_modified:
|
||||||
|
# already processed
|
||||||
|
break
|
||||||
|
sccs[i] = frozenset(old_scc_modified)
|
||||||
|
sccs.insert(i, frozenset(new_scc))
|
||||||
|
break
|
||||||
|
return sccs
|
||||||
|
|
||||||
|
build.sorted_components = patched_sorted_components
|
||||||
|
|||||||
74
mypy_django_plugin/monkeypatch/multiple_inheritance.py
Normal file
74
mypy_django_plugin/monkeypatch/multiple_inheritance.py
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from mypy.checkmember import bind_self, is_final_node, type_object_type
|
||||||
|
from mypy.nodes import TypeInfo, Context, SymbolTableNode, FuncBase
|
||||||
|
from mypy.subtypes import is_subtype, is_equivalent
|
||||||
|
from mypy.types import FunctionLike, CallableType, Type
|
||||||
|
|
||||||
|
|
||||||
|
def make_inner_classes_with_inherit_from_any_compatible_with_each_other():
|
||||||
|
from mypy.checker import TypeChecker
|
||||||
|
|
||||||
|
def determine_type_of_class_member(self, sym: SymbolTableNode) -> Optional[Type]:
|
||||||
|
if sym.type is not None:
|
||||||
|
return sym.type
|
||||||
|
if isinstance(sym.node, FuncBase):
|
||||||
|
return self.function_type(sym.node)
|
||||||
|
if isinstance(sym.node, TypeInfo):
|
||||||
|
# nested class
|
||||||
|
return type_object_type(sym.node, self.named_type)
|
||||||
|
return None
|
||||||
|
|
||||||
|
TypeChecker.determine_type_of_class_member = determine_type_of_class_member
|
||||||
|
|
||||||
|
def check_compatibility(self, name: str, base1: TypeInfo,
|
||||||
|
base2: TypeInfo, ctx: Context) -> None:
|
||||||
|
"""Check if attribute name in base1 is compatible with base2 in multiple inheritance.
|
||||||
|
Assume base1 comes before base2 in the MRO, and that base1 and base2 don't have
|
||||||
|
a direct subclass relationship (i.e., the compatibility requirement only derives from
|
||||||
|
multiple inheritance).
|
||||||
|
"""
|
||||||
|
if name in ('__init__', '__new__', '__init_subclass__'):
|
||||||
|
# __init__ and friends can be incompatible -- it's a special case.
|
||||||
|
return
|
||||||
|
first = base1[name]
|
||||||
|
second = base2[name]
|
||||||
|
first_type = self.determine_type_of_class_member(first)
|
||||||
|
second_type = self.determine_type_of_class_member(second)
|
||||||
|
|
||||||
|
# TODO: What if some classes are generic?
|
||||||
|
if (isinstance(first_type, FunctionLike) and
|
||||||
|
isinstance(second_type, FunctionLike)):
|
||||||
|
if ((isinstance(first_type, CallableType)
|
||||||
|
and first_type.fallback.type.fullname() == 'builtins.type')
|
||||||
|
and (isinstance(second_type, CallableType)
|
||||||
|
and second_type.fallback.type.fullname() == 'builtins.type')):
|
||||||
|
# Both members are classes (not necessary nested), check if compatible
|
||||||
|
ok = is_subtype(first_type.ret_type, second_type.ret_type)
|
||||||
|
else:
|
||||||
|
# Method override
|
||||||
|
first_sig = bind_self(first_type)
|
||||||
|
second_sig = bind_self(second_type)
|
||||||
|
ok = is_subtype(first_sig, second_sig, ignore_pos_arg_names=True)
|
||||||
|
elif first_type and second_type:
|
||||||
|
ok = is_equivalent(first_type, second_type)
|
||||||
|
else:
|
||||||
|
if first_type is None:
|
||||||
|
self.msg.cannot_determine_type_in_base(name, base1.name(), ctx)
|
||||||
|
if second_type is None:
|
||||||
|
self.msg.cannot_determine_type_in_base(name, base2.name(), ctx)
|
||||||
|
ok = True
|
||||||
|
# Final attributes can never be overridden, but can override
|
||||||
|
# non-final read-only attributes.
|
||||||
|
if is_final_node(second.node):
|
||||||
|
self.msg.cant_override_final(name, base2.name(), ctx)
|
||||||
|
if is_final_node(first.node):
|
||||||
|
self.check_no_writable(name, second.node, ctx)
|
||||||
|
# __slots__ is special and the type can vary across class hierarchy.
|
||||||
|
if name == '__slots__':
|
||||||
|
ok = True
|
||||||
|
if not ok:
|
||||||
|
self.msg.base_class_definitions_incompatible(name, base1, base2,
|
||||||
|
ctx)
|
||||||
|
|
||||||
|
TypeChecker.check_compatibility = check_compatibility
|
||||||
41
mypy_django_plugin/plugins/fields.py
Normal file
41
mypy_django_plugin/plugins/fields.py
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
from typing import Iterator, List, cast
|
||||||
|
|
||||||
|
from mypy.nodes import ClassDef, AssignmentStmt, CallExpr
|
||||||
|
from mypy.plugin import FunctionContext, ClassDefContext
|
||||||
|
from mypy.semanal import SemanticAnalyzerPass2
|
||||||
|
from mypy.types import Type, Instance
|
||||||
|
|
||||||
|
from mypy_django_plugin.plugins.related_fields import add_new_var_node_to_class
|
||||||
|
|
||||||
|
|
||||||
|
def determine_type_of_array_field(ctx: FunctionContext) -> Type:
|
||||||
|
if 'base_field' not in ctx.arg_names:
|
||||||
|
return ctx.default_return_type
|
||||||
|
|
||||||
|
base_field_arg_type = ctx.arg_types[ctx.arg_names.index('base_field')][0]
|
||||||
|
return ctx.api.named_generic_type(ctx.context.callee.fullname,
|
||||||
|
args=[base_field_arg_type.type.names['__get__'].type.ret_type])
|
||||||
|
|
||||||
|
|
||||||
|
def get_assignments(klass: ClassDef) -> List[AssignmentStmt]:
|
||||||
|
stmts = []
|
||||||
|
for stmt in klass.defs.body:
|
||||||
|
if not isinstance(stmt, AssignmentStmt):
|
||||||
|
continue
|
||||||
|
if len(stmt.lvalues) > 1:
|
||||||
|
# not supported yet
|
||||||
|
continue
|
||||||
|
stmts.append(stmt)
|
||||||
|
return stmts
|
||||||
|
|
||||||
|
|
||||||
|
def add_int_id_attribute_if_primary_key_true_is_not_present(ctx: ClassDefContext) -> None:
|
||||||
|
api = cast(SemanticAnalyzerPass2, ctx.api)
|
||||||
|
for stmt in get_assignments(ctx.cls):
|
||||||
|
if (isinstance(stmt.rvalue, CallExpr)
|
||||||
|
and 'primary_key' in stmt.rvalue.arg_names
|
||||||
|
and api.parse_bool(stmt.rvalue.args[stmt.rvalue.arg_names.index('primary_key')])):
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
add_new_var_node_to_class(ctx.cls.info, 'id', api.builtin_type('builtins.int'))
|
||||||
|
|
||||||
12
mypy_django_plugin/plugins/meta_inner_class.py
Normal file
12
mypy_django_plugin/plugins/meta_inner_class.py
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
from mypy.nodes import TypeInfo
|
||||||
|
from mypy.plugin import ClassDefContext
|
||||||
|
|
||||||
|
|
||||||
|
def inject_any_as_base_for_nested_class_meta(ctx: ClassDefContext) -> None:
|
||||||
|
if 'Meta' not in ctx.cls.info.names:
|
||||||
|
return None
|
||||||
|
sym = ctx.cls.info.names['Meta']
|
||||||
|
if not isinstance(sym.node, TypeInfo):
|
||||||
|
return None
|
||||||
|
|
||||||
|
sym.node.fallback_to_any = True
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
from mypy.plugin import FunctionContext
|
|
||||||
from mypy.types import Type
|
|
||||||
|
|
||||||
|
|
||||||
def determine_type_of_array_field(ctx: FunctionContext) -> Type:
|
|
||||||
if 'base_field' not in ctx.arg_names:
|
|
||||||
return ctx.default_return_type
|
|
||||||
|
|
||||||
base_field_arg_type = ctx.arg_types[ctx.arg_names.index('base_field')][0]
|
|
||||||
return ctx.api.named_generic_type(ctx.context.callee.fullname,
|
|
||||||
args=[base_field_arg_type.type.names['__get__'].type.ret_type])
|
|
||||||
@@ -3,15 +3,16 @@ from typing import Optional, cast
|
|||||||
|
|
||||||
from django.conf import Settings
|
from django.conf import Settings
|
||||||
from mypy.checker import TypeChecker
|
from mypy.checker import TypeChecker
|
||||||
from mypy.nodes import SymbolTable, MDEF, AssignmentStmt
|
from mypy.nodes import MDEF, AssignmentStmt, MypyFile, StrExpr, TypeInfo, NameExpr, Var, SymbolTableNode
|
||||||
from mypy.plugin import FunctionContext, ClassDefContext
|
from mypy.plugin import FunctionContext, ClassDefContext
|
||||||
from mypy.types import Type, CallableType, Instance, AnyType, TypeOfAny
|
from mypy.types import Type, CallableType, Instance, AnyType, TypeOfAny
|
||||||
|
|
||||||
from mypy_django_plugin import helpers
|
from mypy_django_plugin import helpers
|
||||||
|
from mypy_django_plugin.helpers import get_models_file
|
||||||
|
|
||||||
|
|
||||||
def extract_related_name_value(ctx: FunctionContext) -> str:
|
def extract_related_name_value(ctx: FunctionContext) -> str:
|
||||||
return ctx.context.args[ctx.arg_names.index('related_name')].value
|
return ctx.args[ctx.arg_names.index('related_name')][0].value
|
||||||
|
|
||||||
|
|
||||||
def reparametrize_with(instance: Instance, new_typevars: typing.List[Type]):
|
def reparametrize_with(instance: Instance, new_typevars: typing.List[Type]):
|
||||||
@@ -31,8 +32,15 @@ def get_valid_to_value_or_none(ctx: FunctionContext) -> Optional[Instance]:
|
|||||||
|
|
||||||
arg_type = ctx.arg_types[ctx.arg_names.index('to')][0]
|
arg_type = ctx.arg_types[ctx.arg_names.index('to')][0]
|
||||||
if not isinstance(arg_type, CallableType):
|
if not isinstance(arg_type, CallableType):
|
||||||
# to= defined as string is not supported
|
to_arg_expr = ctx.args[ctx.arg_names.index('to')][0]
|
||||||
|
if not isinstance(to_arg_expr, StrExpr):
|
||||||
|
# not string, not supported
|
||||||
return None
|
return None
|
||||||
|
model_info = helpers.get_model_type_from_string(to_arg_expr,
|
||||||
|
all_modules=cast(TypeChecker, ctx.api).modules)
|
||||||
|
if model_info is None:
|
||||||
|
return None
|
||||||
|
return Instance(model_info, [])
|
||||||
|
|
||||||
referred_to_type = arg_type.ret_type
|
referred_to_type = arg_type.ret_type
|
||||||
for base in referred_to_type.type.bases:
|
for base in referred_to_type.type.bases:
|
||||||
@@ -47,59 +55,38 @@ def get_valid_to_value_or_none(ctx: FunctionContext) -> Optional[Instance]:
|
|||||||
return referred_to_type
|
return referred_to_type
|
||||||
|
|
||||||
|
|
||||||
class ForeignKeyHook(object):
|
def add_new_var_node_to_class(class_type: TypeInfo, name: str, typ: Instance) -> None:
|
||||||
def __init__(self, settings: Settings):
|
var = Var(name=name, type=typ)
|
||||||
self.settings = settings
|
var.info = typ.type
|
||||||
|
var._fullname = class_type.fullname() + '.' + name
|
||||||
|
var.is_inferred = True
|
||||||
|
var.is_initialized_in_class = True
|
||||||
|
class_type.names[name] = SymbolTableNode(MDEF, var)
|
||||||
|
|
||||||
def __call__(self, ctx: FunctionContext) -> Type:
|
|
||||||
api = cast(TypeChecker, ctx.api)
|
|
||||||
outer_class_info = api.tscope.classes[-1]
|
|
||||||
|
|
||||||
|
def extract_to_parameter_as_get_ret_type(ctx: FunctionContext) -> Type:
|
||||||
referred_to_type = get_valid_to_value_or_none(ctx)
|
referred_to_type = get_valid_to_value_or_none(ctx)
|
||||||
if referred_to_type is None:
|
if referred_to_type is None:
|
||||||
|
# couldn't extract to= value
|
||||||
return fill_typevars_with_any(ctx.default_return_type)
|
return fill_typevars_with_any(ctx.default_return_type)
|
||||||
|
|
||||||
if 'related_name' in ctx.arg_names:
|
|
||||||
related_name = extract_related_name_value(ctx)
|
|
||||||
queryset_type = api.named_generic_type(helpers.QUERYSET_CLASS_FULLNAME,
|
|
||||||
args=[Instance(outer_class_info, [])])
|
|
||||||
sym = helpers.create_new_symtable_node(related_name, MDEF,
|
|
||||||
instance=queryset_type)
|
|
||||||
referred_to_type.type.names[related_name] = sym
|
|
||||||
|
|
||||||
return reparametrize_with(ctx.default_return_type, [referred_to_type])
|
|
||||||
|
|
||||||
|
|
||||||
class OneToOneFieldHook(object):
|
|
||||||
def __init__(self, settings: Optional[Settings]):
|
|
||||||
self.settings = settings
|
|
||||||
|
|
||||||
def __call__(self, ctx: FunctionContext) -> Type:
|
|
||||||
api = cast(TypeChecker, ctx.api)
|
|
||||||
outer_class_info = api.tscope.classes[-1]
|
|
||||||
|
|
||||||
referred_to_type = get_valid_to_value_or_none(ctx)
|
|
||||||
if referred_to_type is None:
|
|
||||||
return fill_typevars_with_any(ctx.default_return_type)
|
|
||||||
|
|
||||||
if 'related_name' in ctx.arg_names:
|
|
||||||
related_name = extract_related_name_value(ctx)
|
|
||||||
sym = helpers.create_new_symtable_node(related_name, MDEF,
|
|
||||||
instance=Instance(outer_class_info, []))
|
|
||||||
referred_to_type.type.names[related_name] = sym
|
|
||||||
|
|
||||||
return reparametrize_with(ctx.default_return_type, [referred_to_type])
|
return reparametrize_with(ctx.default_return_type, [referred_to_type])
|
||||||
|
|
||||||
|
|
||||||
def set_fieldname_attrs_for_related_fields(ctx: ClassDefContext) -> None:
|
def set_fieldname_attrs_for_related_fields(ctx: ClassDefContext) -> None:
|
||||||
api = ctx.api
|
api = ctx.api
|
||||||
|
for stmt in ctx.cls.defs.body:
|
||||||
new_symtable_nodes = SymbolTable()
|
|
||||||
for (name, symtable_node), stmt in zip(ctx.cls.info.names.items(), ctx.cls.defs.body):
|
|
||||||
if not isinstance(stmt, AssignmentStmt):
|
if not isinstance(stmt, AssignmentStmt):
|
||||||
continue
|
continue
|
||||||
if not hasattr(stmt.rvalue, 'callee'):
|
if not hasattr(stmt.rvalue, 'callee'):
|
||||||
continue
|
continue
|
||||||
|
if len(stmt.lvalues) > 1:
|
||||||
|
# multiple lvalues not supported for now
|
||||||
|
continue
|
||||||
|
|
||||||
|
expr = stmt.lvalues[0]
|
||||||
|
if not isinstance(expr, NameExpr):
|
||||||
|
continue
|
||||||
|
name = expr.name
|
||||||
|
|
||||||
rvalue_callee = stmt.rvalue.callee
|
rvalue_callee = stmt.rvalue.callee
|
||||||
if rvalue_callee.fullname in {helpers.FOREIGN_KEY_FULLNAME,
|
if rvalue_callee.fullname in {helpers.FOREIGN_KEY_FULLNAME,
|
||||||
@@ -108,7 +95,4 @@ def set_fieldname_attrs_for_related_fields(ctx: ClassDefContext) -> None:
|
|||||||
new_node = helpers.create_new_symtable_node(name,
|
new_node = helpers.create_new_symtable_node(name,
|
||||||
kind=MDEF,
|
kind=MDEF,
|
||||||
instance=api.named_type('__builtins__.int'))
|
instance=api.named_type('__builtins__.int'))
|
||||||
new_symtable_nodes[name] = new_node
|
ctx.cls.info.names[name] = new_node
|
||||||
|
|
||||||
for name, node in new_symtable_nodes.items():
|
|
||||||
ctx.cls.info.names[name] = node
|
|
||||||
|
|||||||
@@ -4,7 +4,5 @@ testpaths = ./test
|
|||||||
python_files = test*.py
|
python_files = test*.py
|
||||||
addopts =
|
addopts =
|
||||||
--tb=native
|
--tb=native
|
||||||
-v
|
|
||||||
-s
|
|
||||||
--ignore=./external
|
--ignore=./external
|
||||||
--mypy-ini-file=./test/plugins.ini
|
--mypy-ini-file=./test/plugins.ini
|
||||||
@@ -14,3 +14,47 @@ reveal_type(user.small_int) # E: Revealed type is 'builtins.int'
|
|||||||
reveal_type(user.name) # E: Revealed type is 'builtins.str'
|
reveal_type(user.name) # E: Revealed type is 'builtins.str'
|
||||||
reveal_type(user.slug) # E: Revealed type is 'builtins.str'
|
reveal_type(user.slug) # E: Revealed type is 'builtins.str'
|
||||||
reveal_type(user.text) # E: Revealed type is 'builtins.str'
|
reveal_type(user.text) # E: Revealed type is 'builtins.str'
|
||||||
|
|
||||||
|
[CASE test_add_id_field_if_no_primary_key_defined]
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class User(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
reveal_type(User().id) # E: Revealed type is 'builtins.int'
|
||||||
|
|
||||||
|
[CASE test_do_not_add_id_if_field_with_primary_key_True_defined]
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class User(models.Model):
|
||||||
|
my_pk = models.IntegerField(primary_key=True)
|
||||||
|
|
||||||
|
reveal_type(User().my_pk) # E: Revealed type is 'builtins.int'
|
||||||
|
reveal_type(User().id) # E: Revealed type is 'Any'
|
||||||
|
[out]
|
||||||
|
main:7: error: "User" has no attribute "id"
|
||||||
|
|
||||||
|
[CASE test_meta_nested_class_allows_subclassing_in_multiple_inheritance]
|
||||||
|
from typing import Any
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class Mixin1(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class Mixin2(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
class User(Mixin1, Mixin2):
|
||||||
|
pass
|
||||||
|
[out]
|
||||||
|
|
||||||
|
[CASE test_inheritance_from_abstract_model_does_not_fail_if_field_with_id_exists]
|
||||||
|
from django.db import models
|
||||||
|
class Abstract(models.Model):
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
class User(Abstract):
|
||||||
|
id = models.AutoField(primary_key=True)
|
||||||
|
[out]
|
||||||
|
|||||||
@@ -21,11 +21,14 @@ class Publisher(models.Model):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
class Book(models.Model):
|
class Book(models.Model):
|
||||||
publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE,
|
publisher = models.ForeignKey(to=Publisher, on_delete=models.CASCADE)
|
||||||
related_name='books')
|
class StylesheetError(Exception):
|
||||||
|
pass
|
||||||
|
owner = models.ForeignKey(db_column='model_id', to='db.Unknown', on_delete=models.CASCADE)
|
||||||
|
|
||||||
book = Book()
|
book = Book()
|
||||||
reveal_type(book.publisher_id) # E: Revealed type is 'builtins.int'
|
reveal_type(book.publisher_id) # E: Revealed type is 'builtins.int'
|
||||||
|
reveal_type(book.owner_id) # E: Revealed type is 'builtins.int'
|
||||||
|
|
||||||
[CASE test_foreign_key_field_different_order_of_params]
|
[CASE test_foreign_key_field_different_order_of_params]
|
||||||
from django.db import models
|
from django.db import models
|
||||||
@@ -36,18 +39,77 @@ class Publisher(models.Model):
|
|||||||
class Book(models.Model):
|
class Book(models.Model):
|
||||||
publisher = models.ForeignKey(on_delete=models.CASCADE, to=Publisher,
|
publisher = models.ForeignKey(on_delete=models.CASCADE, to=Publisher,
|
||||||
related_name='books')
|
related_name='books')
|
||||||
|
publisher2 = models.ForeignKey(to=Publisher, related_name='books2', on_delete=models.CASCADE)
|
||||||
|
|
||||||
book = Book()
|
book = Book()
|
||||||
reveal_type(book.publisher) # E: Revealed type is 'main.Publisher*'
|
reveal_type(book.publisher) # E: Revealed type is 'main.Publisher*'
|
||||||
|
reveal_type(book.publisher2) # E: Revealed type is 'main.Publisher*'
|
||||||
|
|
||||||
publisher = Publisher()
|
publisher = Publisher()
|
||||||
reveal_type(publisher.books) # E: Revealed type is 'django.db.models.query.QuerySet[main.Book]'
|
reveal_type(publisher.books) # E: Revealed type is 'django.db.models.query.QuerySet[main.Book]'
|
||||||
|
reveal_type(publisher.books2) # E: Revealed type is 'django.db.models.query.QuerySet[main.Book]'
|
||||||
|
|
||||||
[CASE test_to_parameter_as_string_fallbacks_to_any]
|
[CASE test_to_parameter_as_string_with_application_name__model_imported]
|
||||||
|
from django.db import models
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from myapp.models import Publisher
|
||||||
|
|
||||||
|
class Book(models.Model):
|
||||||
|
publisher = models.ForeignKey(to='myapp.Publisher', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
book = Book()
|
||||||
|
reveal_type(book.publisher) # E: Revealed type is 'myapp.models.Publisher*'
|
||||||
|
|
||||||
|
[file myapp/__init__.py]
|
||||||
|
[file myapp/models.py]
|
||||||
|
from django.db import models
|
||||||
|
class Publisher(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
[CASE test_to_parameter_as_string_with_application_name__fallbacks_to_any_if_model_not_present_in_dependency_graph]
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
class Book(models.Model):
|
class Book(models.Model):
|
||||||
publisher = models.ForeignKey(to='Publisher', on_delete=models.CASCADE)
|
publisher = models.ForeignKey(to='myapp.Publisher', on_delete=models.CASCADE)
|
||||||
|
|
||||||
book = Book()
|
book = Book()
|
||||||
reveal_type(book.publisher) # E: Revealed type is 'Any'
|
reveal_type(book.publisher) # E: Revealed type is 'Any'
|
||||||
|
|
||||||
|
[file myapp/__init__.py]
|
||||||
|
[file myapp/models.py]
|
||||||
|
from django.db import models
|
||||||
|
class Publisher(models.Model):
|
||||||
|
pass
|
||||||
|
|
||||||
|
[CASE test_circular_dependency_in_imports_with_foreign_key]
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
class App(models.Model):
|
||||||
|
def method(self) -> None:
|
||||||
|
reveal_type(self.views) # E: Revealed type is 'django.db.models.query.QuerySet[main.View]'
|
||||||
|
reveal_type(self.members) # E: Revealed type is 'django.db.models.query.QuerySet[main.Member]'
|
||||||
|
reveal_type(self.sheets) # E: Revealed type is 'django.db.models.query.QuerySet[main.Sheet]'
|
||||||
|
reveal_type(self.profile) # E: Revealed type is 'main.Profile'
|
||||||
|
class View(models.Model):
|
||||||
|
app = models.ForeignKey(to=App, related_name='views', on_delete=models.CASCADE)
|
||||||
|
class Member(models.Model):
|
||||||
|
app = models.ForeignKey(related_name='members', on_delete=models.CASCADE, to=App)
|
||||||
|
class Sheet(models.Model):
|
||||||
|
app = models.ForeignKey(App, related_name='sheets', on_delete=models.CASCADE)
|
||||||
|
class Profile(models.Model):
|
||||||
|
app = models.OneToOneField(App, related_name='profile', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
[CASE test_circular_dependency_in_imports_with_string_based]
|
||||||
|
from django.db import models
|
||||||
|
from myapp.models import App
|
||||||
|
class View(models.Model):
|
||||||
|
app = models.ForeignKey(to='myapp.App', related_name='views', on_delete=models.CASCADE)
|
||||||
|
|
||||||
|
[file myapp/__init__.py]
|
||||||
|
[file myapp/models.py]
|
||||||
|
from django.db import models
|
||||||
|
class App(models.Model):
|
||||||
|
def method(self) -> None:
|
||||||
|
reveal_type(self.views) # E: Revealed type is 'django.db.models.query.QuerySet[main.View]'
|
||||||
|
|||||||
@@ -19,11 +19,36 @@ OBJ = LazyObject()
|
|||||||
[CASE test_settings_could_be_defined_in_different_module_and_imported_with_star]
|
[CASE test_settings_could_be_defined_in_different_module_and_imported_with_star]
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
|
||||||
reveal_type(settings.BASE) # E: Revealed type is 'builtins.int'
|
reveal_type(settings.ROOT_DIR) # E: Revealed type is 'pathlib.Path'
|
||||||
reveal_type(settings.NESTED) # E: Revealed type is 'builtins.str'
|
reveal_type(settings.SETUP) # E: Revealed type is 'builtins.int'
|
||||||
|
reveal_type(settings.DATABASES) # E: Revealed type is 'builtins.dict[builtins.str, builtins.str]'
|
||||||
[env DJANGO_SETTINGS_MODULE=mysettings]
|
[env DJANGO_SETTINGS_MODULE=mysettings]
|
||||||
[file mysettings.py]
|
[file mysettings.py]
|
||||||
|
from local import *
|
||||||
|
DATABASES = {'default': 'mydb'}
|
||||||
|
[file local.py]
|
||||||
from base import *
|
from base import *
|
||||||
NESTED = '1122'
|
SETUP = 3
|
||||||
[file base.py]
|
[file base.py]
|
||||||
BASE = 1
|
from pathlib import Path
|
||||||
|
|
||||||
|
ROOT_DIR = Path(__file__)
|
||||||
|
|
||||||
|
[CASE test_circular_dependency_in_settings]
|
||||||
|
from django.conf import settings
|
||||||
|
|
||||||
|
class Class:
|
||||||
|
pass
|
||||||
|
|
||||||
|
reveal_type(settings.MYSETTING) # E: Revealed type is 'builtins.int'
|
||||||
|
reveal_type(settings.REGISTRY) # E: Revealed type is 'Any'
|
||||||
|
|
||||||
|
[file mysettings.py]
|
||||||
|
from typing import TYPE_CHECKING
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from .main import Class
|
||||||
|
|
||||||
|
MYSETTING = 1122
|
||||||
|
REGISTRY: Optional['Class'] = None
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user