mirror of
https://github.com/davidhalter/django-stubs.git
synced 2025-12-08 13:04:47 +08:00
Disable monkeypatches, add dependencies via new hook (#60)
* code cleanups, disable monkeypatches, move to add_additional_deps * disable incremental mode for tests * add pip-wheel-metadata * move some code from get_base_hook to get_attribute_hook to reduce dependencies * simplify values/values_list tests and code * disable cache for some tests failing with incremental mode * enable incremental mode for tests typechecking * pin mypy version * fix tests * lint * fix internal crashes
This commit is contained in:
@@ -1,97 +1,78 @@
|
||||
from typing import Iterable, List, Optional, cast
|
||||
from typing import TYPE_CHECKING, List, Optional, cast
|
||||
|
||||
from mypy.nodes import (
|
||||
ClassDef, Context, ImportAll, MypyFile, SymbolNode, SymbolTableNode, TypeInfo, Var,
|
||||
)
|
||||
from mypy.plugin import ClassDefContext
|
||||
from mypy.semanal import SemanticAnalyzerPass2
|
||||
from mypy.types import AnyType, Instance, NoneTyp, Type, TypeOfAny, UnionType
|
||||
from mypy.util import correct_relative_import
|
||||
from mypy.checkexpr import FunctionContext
|
||||
from mypy.checkmember import AttributeContext
|
||||
from mypy.nodes import NameExpr, StrExpr, SymbolTableNode, TypeInfo
|
||||
from mypy.types import AnyType, Instance, Type, TypeOfAny, TypeType
|
||||
|
||||
from mypy_django_plugin import helpers
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from mypy.checker import TypeChecker
|
||||
|
||||
|
||||
def get_error_context(node: SymbolNode) -> Context:
|
||||
context = Context()
|
||||
context.set_line(node)
|
||||
return context
|
||||
def get_setting_sym(name: str, api: 'TypeChecker', settings_modules: List[str]) -> Optional[SymbolTableNode]:
|
||||
for settings_mod_name in settings_modules:
|
||||
file = api.modules[settings_mod_name]
|
||||
sym = file.names.get(name)
|
||||
if sym is not None:
|
||||
return sym
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def filter_out_nones(typ: UnionType) -> List[Type]:
|
||||
return [item for item in typ.items if not isinstance(item, NoneTyp)]
|
||||
def get_type_of_setting(ctx: AttributeContext, setting_name: str,
|
||||
settings_modules: List[str], ignore_missing_settings: bool) -> Type:
|
||||
setting_sym = get_setting_sym(setting_name, ctx.api, settings_modules)
|
||||
if setting_sym:
|
||||
if setting_sym.type is None:
|
||||
# TODO: defer till setting_sym.type is not None
|
||||
return AnyType(TypeOfAny.implementation_artifact)
|
||||
|
||||
return setting_sym.type
|
||||
|
||||
if not ignore_missing_settings:
|
||||
ctx.api.fail(f"'Settings' object has no attribute {setting_name!r}", ctx.context)
|
||||
|
||||
return ctx.default_attr_type
|
||||
|
||||
|
||||
def make_sym_copy_of_setting(sym: SymbolTableNode) -> Optional[SymbolTableNode]:
|
||||
if isinstance(sym.type, Instance):
|
||||
copied = sym.copy()
|
||||
copied.node.info = sym.type.type
|
||||
return copied
|
||||
elif isinstance(sym.type, UnionType):
|
||||
instances = filter_out_nones(sym.type)
|
||||
if len(instances) > 1:
|
||||
# plain unions not supported yet
|
||||
return None
|
||||
typ = instances[0]
|
||||
if isinstance(typ, Instance):
|
||||
copied = sym.copy()
|
||||
copied.node.info = typ.type
|
||||
return copied
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
def return_user_model_hook(ctx: FunctionContext, settings_modules: List[str]) -> Type:
|
||||
from mypy.checker import TypeChecker
|
||||
|
||||
api = cast(TypeChecker, ctx.api)
|
||||
|
||||
def get_settings_metadata(lazy_settings_info: TypeInfo):
|
||||
return lazy_settings_info.metadata.setdefault('django', {}).setdefault('settings', {})
|
||||
setting_sym = get_setting_sym('AUTH_USER_MODEL', api, settings_modules)
|
||||
if setting_sym is None:
|
||||
return ctx.default_return_type
|
||||
|
||||
setting_module_name, _, _ = setting_sym.fullname.rpartition('.')
|
||||
setting_module = api.modules[setting_module_name]
|
||||
|
||||
def load_settings_from_names(settings_classdef: ClassDef,
|
||||
modules: Iterable[MypyFile],
|
||||
api: SemanticAnalyzerPass2) -> None:
|
||||
settings_metadata = get_settings_metadata(settings_classdef.info)
|
||||
model_path = None
|
||||
for name_expr, rvalue_expr in helpers.iter_over_assignments(setting_module):
|
||||
if isinstance(name_expr, NameExpr) and isinstance(rvalue_expr, StrExpr):
|
||||
if name_expr.name == 'AUTH_USER_MODEL':
|
||||
model_path = rvalue_expr.value
|
||||
break
|
||||
|
||||
for module in modules:
|
||||
for name, sym in module.names.items():
|
||||
if name.isupper() and isinstance(sym.node, Var):
|
||||
if sym.type is not None:
|
||||
copied = make_sym_copy_of_setting(sym)
|
||||
if copied is None:
|
||||
continue
|
||||
settings_classdef.info.names[name] = copied
|
||||
else:
|
||||
var = Var(name, AnyType(TypeOfAny.unannotated))
|
||||
var.info = api.named_type('__builtins__.object').type # outer class type
|
||||
settings_classdef.info.names[name] = SymbolTableNode(sym.kind, var, plugin_generated=True)
|
||||
if not model_path:
|
||||
return ctx.default_return_type
|
||||
|
||||
settings_metadata[name] = module.fullname()
|
||||
app_label, _, model_class_name = model_path.rpartition('.')
|
||||
if app_label is None:
|
||||
return ctx.default_return_type
|
||||
|
||||
model_fullname = helpers.get_model_fullname(app_label, model_class_name,
|
||||
all_modules=api.modules)
|
||||
if model_fullname is None:
|
||||
api.fail(f'"{app_label}.{model_class_name}" model class is not imported so far. Try to import it '
|
||||
f'(under if TYPE_CHECKING) at the beginning of the current file',
|
||||
context=ctx.context)
|
||||
return ctx.default_return_type
|
||||
|
||||
def get_import_star_modules(api: SemanticAnalyzerPass2, module: MypyFile) -> List[str]:
|
||||
import_star_modules = []
|
||||
for module_import in module.imports:
|
||||
# relative import * are not resolved by mypy
|
||||
if isinstance(module_import, ImportAll) and module_import.relative:
|
||||
absolute_import_path, correct = correct_relative_import(module.fullname(), module_import.relative,
|
||||
module_import.id, is_cur_package_init_file=False)
|
||||
if not correct:
|
||||
return []
|
||||
for path in [absolute_import_path] + get_import_star_modules(api,
|
||||
module=api.modules.get(absolute_import_path)):
|
||||
if path not in import_star_modules:
|
||||
import_star_modules.append(path)
|
||||
return import_star_modules
|
||||
|
||||
|
||||
class AddSettingValuesToDjangoConfObject:
|
||||
def __init__(self, settings_modules: List[str], ignore_missing_settings: bool):
|
||||
self.settings_modules = settings_modules
|
||||
self.ignore_missing_settings = ignore_missing_settings
|
||||
|
||||
def __call__(self, ctx: ClassDefContext) -> None:
|
||||
api = cast(SemanticAnalyzerPass2, ctx.api)
|
||||
for module_name in self.settings_modules:
|
||||
module = api.modules[module_name]
|
||||
star_deps = [api.modules[star_dep]
|
||||
for star_dep in reversed(get_import_star_modules(api, module))]
|
||||
load_settings_from_names(ctx.cls, modules=star_deps + [module], api=api)
|
||||
|
||||
if self.ignore_missing_settings:
|
||||
ctx.cls.info.fallback_to_any = True
|
||||
model_info = helpers.lookup_fully_qualified_generic(model_fullname,
|
||||
all_modules=api.modules)
|
||||
if model_info is None or not isinstance(model_info, TypeInfo):
|
||||
return ctx.default_return_type
|
||||
return TypeType(Instance(model_info, []))
|
||||
|
||||
Reference in New Issue
Block a user